cover

一句話:好的 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 更花俏。

系列文章

延伸閱讀