---
# System prepended metadata

title: 從 Custom Hook 到 Zustand：React 狀態共享的三種做法
tags: [Zustand, React]

---

# 從 Custom Hook 到 Zustand：React 狀態共享的三種做法

> 當我們在開發 React 應用時，常會不自覺地掉入一個陷阱：**以為把 state 包進 custom hook，就可以實現狀態共享。**
> 事實上，這樣做只會讓每個使用該 hook 的元件擁有「**各自獨立的 state**」，彼此無法同步。

## 什麼是 Custom Hook？

Custom Hook 是 React 提供的強大抽象機制，讓我們能將元件邏輯封裝成可重用的函式。

以下是一個基本的計數器 hook 範例：

```tsx
// useCounter.ts
import { useState } from 'react'

export function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue)

  const increment = () => setCount((c) => c + 1)
  const decrement = () => setCount((c) => c - 1)
  const reset = () => setCount(initialValue)

  return { count, increment, decrement, reset }
}
```

```tsx
// Counter.tsx
import { useCounter } from './useCounter'

export function Counter() {
  const { count, increment, decrement, reset } = useCounter(5)

  return (
    <div>
      <p>目前計數：{count}</p>
      <button onClick={increment}>＋1</button>
      <button onClick={decrement}>－1</button>
      <button onClick={reset}>重設</button>
    </div>
  )
}
```

透過 `useCounter`，我們成功把計數邏輯與 UI 呈現分離，使程式碼更具模組性、可測試性與重複使用性。


## 但 Custom Hook 可以共享狀態嗎？

假設你這樣寫：

```tsx
// useSearchOptions.ts
function useSearchOptions() {
  const [isEnabled, setIsEnabled] = useState(false)
  return { isEnabled, setIsEnabled }
}

// ComponentA.tsx
const { isEnabled } = useSearchOptions()

// ComponentB.tsx
const { isEnabled } = useSearchOptions()
```

### ❌ 答案是：**無法共享**。

即使兩個元件都呼叫 `useSearchOptions()`，但每次 hook 執行時，都會建立一個全新的 state 實例。
也就是說，**`ComponentA` 和 `ComponentB` 各自擁有獨立的 isEnabled**，互不干涉。

🔔 **Hook 是邏輯抽象，不是共享工具！**

## 正確的狀態共享方式

### 1. 父層 state + props 傳遞

```tsx
// Parent.tsx
function Parent() {
  const [isEnabled, setIsEnabled] = useState(false)
  return (
    <div>
      <ComponentA isEnabled={isEnabled} />
      <ComponentB setIsEnabled={setIsEnabled} />
    </div>
  )
}

// ComponentA.tsx
function ComponentA({ isEnabled }: { isEnabled: boolean }) {
  return <div>A: {isEnabled.toString()}</div>
}

// ComponentB.tsx
function ComponentB({ setIsEnabled }: { setIsEnabled: (v: boolean) => void }) {
  return <button onClick={() => setIsEnabled(true)}>開啟</button>
}
```

✅ 優點：簡單直覺、符合 React 單向資料流
❌ 缺點：會產生 `props drilling`，不適合層級複雜的應用

### 2. 使用 React Context

```tsx
// SearchContext.tsx
import { createContext, useContext, useState } from 'react'

type SearchContextType = {
  isEnabled: boolean
  setIsEnabled: (v: boolean) => void
}

const SearchContext = createContext<SearchContextType | null>(null)

export function SearchProvider({ children }: { children: React.ReactNode }) {
  const [isEnabled, setIsEnabled] = useState(false)
  return (
    <SearchContext.Provider value={{ isEnabled, setIsEnabled }}>
      {children}
    </SearchContext.Provider>
  )
}

export function useSearchContext() {
  const ctx = useContext(SearchContext)
  if (!ctx) throw new Error('useSearchContext 必須在 SearchProvider 中使用')
  return ctx
}
```

```tsx
// ComponentA.tsx
function ComponentA() {
  const { isEnabled } = useSearchContext()
  return <div>A: {isEnabled.toString()}</div>
}

// ComponentB.tsx
function ComponentB() {
  const { setIsEnabled } = useSearchContext()
  return <button onClick={() => setIsEnabled(true)}>開啟</button>
}
```

✅ 優點：解決 props drilling 問題，可跨層級共用狀態
❌ 缺點：只要 `value` 改變，整個 provider 下的元件都會 re-render（效能瓶頸）

## Zustand：現代、輕量的狀態共享方案

[Zustand](https://zustand-demo.pmnd.rs/)，語法像 hook，效能媲美 Redux，卻無需寫 reducer 或 context。

### 安裝

```bash
npm install zustand
```

### Zustand 實作 Store

```ts
// useSearchStore.ts
import { create } from 'zustand'

export const useSearchStore = create((set) => ({
  isEnabled: false,
  setIsEnabled: (v: boolean) => set({ isEnabled: v }),
}))
```

### 使用 Store

```tsx
// ComponentA.tsx
function ComponentA() {
  const isEnabled = useSearchStore((s) => s.isEnabled)
  return <div>A: {isEnabled.toString()}</div>
}

// ComponentB.tsx
function ComponentB() {
  const setIsEnabled = useSearchStore((s) => s.setIsEnabled)
  return <button onClick={() => setIsEnabled(true)}>開啟</button>
}
```

✅ 只有 `ComponentA` 訂閱了 `isEnabled`，狀態變更時只有它會 re-render，`ComponentB` 不會受到影響。

---

## Zustand 重構 Custom Hook 實例

假設你之前寫了一個封裝邏輯的 custom hook，現在可以這樣改寫成 Zustand 的版本：

### 原本 Custom Hook

```ts
function useSearchOptions() {
  const [isEnabled, setIsEnabled] = useState(false)
  return { isEnabled, setIsEnabled }
}
```

### Zustand 重構

```ts
// useSearchStore.ts
import { create } from 'zustand'

export const useSearchStore = create((set) => ({
  isEnabled: false,
  setIsEnabled: (v: boolean) => set({ isEnabled: v }),
}))
```

你也可以包裝成一樣的 API：

```ts
export function useSearchOptions() {
  const isEnabled = useSearchStore((s) => s.isEnabled)
  const setIsEnabled = useSearchStore((s) => s.setIsEnabled)
  return { isEnabled, setIsEnabled }
}
```

這樣就能在原本使用 `useSearchOptions` 的元件中無痛切換到 Zustand。


## 三種狀態共享方式比較表

| 方法            | 優點                     | 缺點                         | 適用場景      |
| ------------- | ---------------------- | -------------------------- | --------- |
| Props 傳遞      | 簡單直觀，資料流清晰             | props drilling，耦合性高        | 小型應用，局部共享 |
| React Context | 跨層級元件共享方便              | Provider 下所有元件都會 re-render | 中大型應用     |
| Zustand       | 輕量精準訂閱、效能佳、無需 Provider | 額外學習曲線，需導入外部套件             | 中大型、複雜邏輯  |



## 總結：Custom Hook ≠ 狀態共享

曾經我在開發中誤以為 hook 是共享的，導致跨元件資料不同步的 bug。這次經驗讓我重新檢視：

> **「在 React 中，應該如何正確設計狀態共享邏輯？」**

以下是我歸納的結論：

* ✅ Custom Hook 用來抽象邏輯，不應該用來共享 state
* ✅ 小範圍可用 props，跨層級可用 context
* ✅ 複雜應用推薦使用 Zustand，效能與開發體驗兼具

## 參考資料：
React: https://react.dev/
Zustand: https://zustand.docs.pmnd.rs/getting-started/introduction

