
一句話:好的 Custom Hook 做一件事,好的效能優化從量測開始,好的專案結構在對的時機出現。
先講結論
上一篇講了元件怎麼分、狀態放哪裡。這篇講三件事:怎麼用 Custom Hooks 把邏輯抽乾淨、效能問題怎麼抓、專案結構什麼時候該動。
Custom Hooks:不是把 code 搬到另一個檔案就叫 Hook
Custom Hooks 是 React 架構的核心武器,但我看過太多人把它當垃圾桶——把一堆不相關的邏輯全部塞進一個 useEverything() 裡面。那不叫抽象,那叫搬家。
好的 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;
}職責單一、命名清晰、回傳值有意義。你看名字就知道它幹嘛,不用讀 source code。
設計原則
單一職責——一個 Hook 做一件事。useUserManagement() OK,useEverything() 不行。
回傳明確——回傳 { users, isLoading, createUser } 比回傳 10 個 boolean 好。使用者不用猜第三個 boolean 是什麼意思。
可組合——好的 Hook 可以被其他 Hook 使用。如果你的 Hook 緊耦合某個特定元件,那它可能不該是 Hook。
Feature Hook 模式
我最推薦的 pattern:為每個功能模組建立一個 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();
// 元件只關心呈現,不關心資料怎麼來
}測試也方便——mock useUserManagement 就好,不用 mock 一堆 API call。
效能:先量測,再優化
React 的效能問題 90% 來自不必要的 re-render。但在你開始到處加 React.memo 之前,先問自己:你有量測過嗎?
三個常見陷阱
在 render 中建立新物件:
// 每次 render 都建立新的 style,導致子元件 re-render
<Child style={{ color: 'red' }} />
// 修正
const style = useMemo(() => ({ color: 'red' }), []);
<Child style={style} />Context 值不穩定:
// 每次 render 都建立新物件,所有 consumer 都 re-render
<ThemeContext.Provider value={{ theme, toggleTheme }}>
// 修正
const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme]);
<ThemeContext.Provider value={value}>每次都建立新 callback:
// 父元件 re-render 就產生新 function
<List items={items} onItemClick={(id) => handleClick(id)} />
// 修正
const onItemClick = useCallback((id) => handleClick(id), [handleClick]);
<List items={items} onItemClick={onItemClick} />但大部分時候,React 夠快
不要過度優化。只有在這些情況才需要認真處理:
- 列表超過 100 項——考慮虛擬化(react-window / react-virtuoso)
- React DevTools Profiler 顯示明顯的卡頓
- 高頻更新——拖曳、動畫、即時輸入
React.memo 到處加不會讓你的 app 更快,只會讓你的 code 更難讀。我見過有人把每個 component 都包 memo,然後問我為什麼還是卡。
專案結構:Feature-Based
src/
├── features/ # 按功能模組分
│ ├── auth/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── api.ts
│ │ └── types.ts
│ ├── users/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── api.ts
│ │ └── types.ts
│ └── dashboard/
├── components/ # 共用 UI 元件
├── hooks/ # 共用 Hooks
├── lib/ # 工具函數、API client
├── pages/ # 路由頁面
└── types/ # 共用型別
核心原則:相關的東西放一起。auth 的 component、hook、type 都在 features/auth/ 裡面,不用跨五個目錄才能完成一個功能。
依賴方向是單向的:features → components,不反向。共用 UI 元件不應該 import features 裡的東西。
什麼時候該重構結構?
不要一開始就追求完美結構。以下是動手的信號:
- 同一個目錄超過 15 個檔案——太擠了
- 經常需要跨多個目錄才能改完一個功能——分散了
- 新人需要超過 30 分鐘才能找到某個功能的 code——迷路了
重構結構的成本其實不高——就是移檔案改 import。但如果你用了絕對路徑 alias,這件事會更愉快。
速查清單
元件:超過 200 行就拆、props 超過 7 個就重新設計、一個元件只做一件事。
狀態:Server State 用 React Query、全域狀態越少越好、能放 URL 就放 URL。
效能:先量測再優化、React.memo 只用在確認有問題的地方、長列表用虛擬化。
TypeScript:Props 用 interface(有 extends 優勢)、API 型別和 Props 型別分開定義、善用 as const 和 discriminated union。
React 的架構不是一開始就要完美,而是在對的時機做對的調整。如果你的程式碼還能跑、還能改、新人還看得懂——那就是好架構。不用跟 Twitter 上的人比誰的 folder structure 更花俏。
系列文章
- (一)元件設計與狀態管理
- (二)Custom Hooks、效能與專案結構(本篇)