
為什麼需要談架構
小型 React 專案隨意寫都能動。但當元件超過 50 個、狀態邏輯交叉耦合、多人協作開始出現衝突,「架構」就不再是可選項。
本文不是 React 入門教學。如果你需要了解 React 的基本概念,請先看 React Hooks。本文聚焦在:你已經會寫 React 了,如何寫出可維護的 React 應用。
元件設計模式
元件分層
flowchart TD subgraph Pages["頁面層 Pages"] P1["UserListPage"] P2["UserDetailPage"] end subgraph Features["功能層 Features"] F1["UserTable"] F2["UserProfile"] F3["UserForm"] end subgraph UI["UI 層"] U1["Button"] U2["Modal"] U3["Table"] U4["Avatar"] end P1 --> F1 P2 --> F2 P2 --> F3 F1 --> U3 F2 --> U4 F3 --> U1 F3 --> U2
| 層級 | 職責 | 特徵 |
|---|---|---|
| Pages | 路由對應、資料載入、Layout 組合 | 知道 URL、知道 API |
| Features | 業務邏輯、功能區塊 | 知道 domain、不知道 URL |
| UI | 純 UI 元件、無業務邏輯 | 只知道 props、可跨專案複用 |
判斷元件該放哪一層的問題:
- 這個元件換到另一個專案還能用嗎?→ UI 層
- 這個元件換到另一個頁面還能用嗎?→ Features 層
- 都不行 → Pages 層
Container / Presentational 分離
雖然 Hooks 讓這個模式不再「必要」,但分離邏輯和呈現的思路仍然重要:
// Container:負責邏輯
function UserListContainer() {
const { users, loading, error } = useUsers();
const handleDelete = useDeleteUser();
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <UserList users={users} onDelete={handleDelete} />;
}
// Presentational:負責呈現
function UserList({ users, onDelete }) {
return (
<ul>
{users.map(user => (
<UserCard key={user.id} user={user} onDelete={onDelete} />
))}
</ul>
);
}為什麼要分離:
- Presentational 元件可以用 Storybook 獨立開發和測試
- 邏輯變更不影響 UI,UI 變更不影響邏輯
- 同一份資料可以有不同的呈現方式(list / grid / table)
Compound Components
當一組元件需要共享隱含的狀態時,使用 Compound Components:
// 使用方式——直覺的宣告式 API
<Tabs defaultValue="profile">
<Tabs.List>
<Tabs.Trigger value="profile">Profile</Tabs.Trigger>
<Tabs.Trigger value="settings">Settings</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="profile">Profile content</Tabs.Content>
<Tabs.Content value="settings">Settings content</Tabs.Content>
</Tabs>Compound Components 的特徵:
- 父元件透過 Context 共享狀態給子元件
- 子元件可以任意組合順序
- API 對使用者來說是宣告式的
適用場景:Tabs、Accordion、Dropdown、Form 等需要元件間隱含狀態同步的 UI。
狀態管理策略
狀態分類
flowchart TD State["狀態"] --> Local["Local State\n元件內部狀態"] State --> Shared["Shared State\n跨元件共享"] State --> Server["Server State\n伺服器資料快取"] State --> URL["URL State\n路由參數、查詢字串"]
| 狀態類型 | 管理方式 | 範例 |
|---|---|---|
| Local | useState / useReducer | 表單輸入、Modal 開關 |
| Shared | Context / Zustand / Jotai | 使用者登入狀態、主題設定 |
| Server | React Query / SWR | API 資料、分頁、快取 |
| URL | React Router | 搜尋條件、分頁頁碼、篩選器 |
選擇原則:先問「這個狀態住在哪裡最自然」
flowchart TD Q1{"只有一個元件用?"} -->|是| Local["useState"] Q1 -->|否| Q2{"來自伺服器?"} Q2 -->|是| Server["React Query / SWR"] Q2 -->|否| Q3{"需要出現在 URL?"} Q3 -->|是| URL["URL Search Params"] Q3 -->|否| Q4{"跨多少元件?"} Q4 -->|"2-3 個相鄰元件"| Lift["提升到共同父元件"] Q4 -->|"多個不相鄰元件"| Global["Zustand / Jotai / Context"]
Server State 不是全域狀態
這是很多人犯的錯誤:把 API 回傳的資料放進 Redux / Context,然後手動管理 loading、error、refetch、cache invalidation。
用 React Query / SWR 就對了:
function UserList() {
const { data: users, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => api.getUsers(),
});
// 不需要:
// - 手動設定 loading state
// - 手動 dispatch action
// - 手動處理 cache
// - 手動處理 refetch
}React Query 自動處理:
- Loading / Error / Success 狀態
- 快取和自動重新驗證
- 背景更新(stale-while-revalidate)
- 重試和請求去重
全域狀態:越少越好
2016: 所有狀態都放 Redux
2020: 伺服器狀態用 React Query,剩下的放 Context
2024: 大多數「全域狀態」其實是 Server State 或 URL State
2026: 真正需要全域管理的狀態其實很少
真正需要全域狀態管理的場景:
- 使用者認證狀態
- 主題 / 語言偏好
- 通知佇列
- 多步驟表單的跨頁資料
Custom Hooks 設計
Custom Hooks 是 React 架構的核心武器——把邏輯從元件中抽離出來。
好的 Custom Hook
// 職責單一、命名清晰、回傳值有意義
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}Custom Hook 設計原則
| 原則 | 說明 | 反例 |
|---|---|---|
| 單一職責 | 一個 Hook 做一件事 | useEverything() |
| 回傳明確 | 回傳值語義清晰 | 回傳 10 個 boolean |
| 可測試 | 不依賴全域狀態 | 直接 import config |
| 可組合 | 可以被其他 Hook 使用 | 緊耦合特定元件 |
Feature Hook 模式
為每個功能模組建立一個 Hook,封裝該功能的所有邏輯:
function useUserManagement() {
const users = useQuery({ queryKey: ['users'], queryFn: api.getUsers });
const createUser = useMutation({ mutationFn: api.createUser });
const deleteUser = useMutation({ mutationFn: api.deleteUser });
return {
users: users.data ?? [],
isLoading: users.isLoading,
createUser: createUser.mutate,
deleteUser: deleteUser.mutate,
isCreating: createUser.isPending,
};
}元件只需要:
function UserPage() {
const { users, isLoading, createUser, deleteUser } = useUserManagement();
// 元件只關心呈現,不關心資料怎麼來的
}效能優化
渲染優化原則
React 的效能問題 90% 來自不必要的 re-render。
flowchart TD Render["元件 re-render"] --> Q1{"真的需要 re-render?"} Q1 -->|"State/Props 沒變"| Memo["React.memo\n阻止不必要的 re-render"] Q1 -->|"有變化"| Q2{"計算量大?"} Q2 -->|是| UseMemo["useMemo\n快取計算結果"] Q2 -->|否| OK["正常 re-render\n React 很快的"]
常見效能陷阱
陷阱 1:在 render 中建立新物件
// 每次 render 都建立新的 style 物件,導致子元件 re-render
<Child style={{ color: 'red' }} />
// 修正:提出去或用 useMemo
const style = useMemo(() => ({ color: 'red' }), []);
<Child style={style} />陷阱 2:Context 值變化導致整棵樹 re-render
// 危險:每次 render 都建立新物件
<ThemeContext.Provider value={{ theme, toggleTheme }}>
// 修正:useMemo 穩定 value
const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme]);
<ThemeContext.Provider value={value}>陷阱 3:在父元件中定義 callback
// 每次父元件 re-render 都建立新 function
<List items={items} onItemClick={(id) => handleClick(id)} />
// 修正:useCallback
const onItemClick = useCallback((id) => handleClick(id), [handleClick]);
<List items={items} onItemClick={onItemClick} />不要過度優化
大多數情況下 React 的 re-render 足夠快。只在以下情況考慮優化:
- 列表超過 100 項
- 有明顯的卡頓(可用 React DevTools Profiler 確認)
- 高頻更新(拖曳、動畫、即時輸入)
專案結構
Feature-Based 結構
src/
├── features/ # 按功能模組分
│ ├── auth/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── api.ts
│ │ └── types.ts
│ ├── users/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── api.ts
│ │ └── types.ts
│ └── dashboard/
├── components/ # 共用 UI 元件
│ ├── Button.tsx
│ ├── Modal.tsx
│ └── Table.tsx
├── hooks/ # 共用 Hooks
│ ├── useDebounce.ts
│ └── useMediaQuery.ts
├── lib/ # 工具函數、API client
├── pages/ # 路由頁面(或 app/ in Next.js)
└── types/ # 共用型別
結構原則
| 原則 | 說明 |
|---|---|
| 功能內聚 | 相關的 component、hook、type 放一起 |
| 依賴方向 | features → components,不反向 |
| 扁平優先 | 不要超過 3 層目錄嵌套 |
| 就近原則 | 只有一個地方用的東西,放在使用者旁邊 |
何時重構結構
不要一開始就追求完美結構。以下是調整時機:
- 同一個目錄超過 15 個檔案
- 經常需要跨多個目錄修改才能完成一個功能
- 新人需要超過 30 分鐘才能找到某個功能的程式碼
實務建議清單
元件設計
- 元件超過 200 行就該拆分
- Props 超過 7 個就該考慮重新設計
- 一個元件只做一件事
狀態管理
- Server State 用 React Query / SWR,不要自己管
- 全域狀態越少越好,先問是否真的需要全域
- URL 能表達的狀態就放 URL(搜尋、篩選、分頁)
效能
- 先量測再優化,不要猜
React.memo只用在確認有效能問題的元件- 長列表用虛擬化(react-window / react-virtuoso)
TypeScript
- 元件 Props 用
interface,不用type(有 extends 優勢) - API 回應型別和元件 Props 型別分開定義
- 善用
as const和 discriminated union
延伸閱讀
- 為什麼用 TypeScript — TypeScript 的必要性
- TypeScript 基礎型別 — TypeScript 型別系統入門
- TypeScript 實務模式 — 進階 TypeScript 設計模式
- React Hooks — React Hooks 的基礎使用