2025/08/20

在大型 React 專案中,我們通常會使用 Zustand 管理全域狀態。但面對複雜的彈窗(Dialog)、獨立頁面模組或多實例元件時,我們往往會在「直接用 Context」與「塞進全域 Store」之間糾結。
很多人會想:「既然是區域狀態,用原生 useContext + useState 不就好了?」
雖然這能解決 Props Drilling,但會帶來嚴重的效能問題:
value 改變時,所有使用了該 Context 的子元件,不論它們只用到哪一部分的資料,通通都會強制重新渲染。count 改變時,A 元件才更新」。createLocalStoreContext 的做法是 「Context 只傳遞 Store 的引用(Reference),而不傳遞狀態(State)」。
value 是 Zustand 的 API 實體,這個引用在元件生命週期內不會改變,因此不會因為狀態更新而觸發 Provider 下層的全面渲染。useStore(selector),元件可以精確地訂閱狀態的某個切片。這個工廠函式的核心在於利用 React Context 傳遞 Zustand 的 StoreApi,並確保 Store 的建立與銷毀跟著元件生命週期走。
1'use client';
2
3import { createContext, useContext, useState, type ReactNode } from 'react';
4import { useStore, type StoreApi } from 'zustand';
5
6export function createLocalStoreContext<TStore>(
7 storeName: string,
8 createStoreFn: () => StoreApi<TStore>,
9) {
10 const StoreContext = createContext<StoreApi<TStore> | null>(null);
11
12 function Provider({ children }: { children: ReactNode }) {
13 // 使用 useState lazy initializer 確保只在掛載時建立一次 store
14 const [store] = useState(() => createStoreFn());
15
16 return (
17 <StoreContext.Provider value={store}>{children}</StoreContext.Provider>
18 );
19 }
20
21 Provider.displayName = `${storeName}Provider`;
22
23 function useLocalStore<T>(selector: (state: TStore) => T): T {
24 const store = useContext(StoreContext);
25 if (!store) {
26 throw new Error(`use${storeName} must be used within <${storeName}Provider>.`);
27 }
28 return useStore(store, selector);
29 }
30
31 function useLocalStoreApi(): StoreApi<TStore> {
32 const store = useContext(StoreContext);
33 if (!store) {
34 throw new Error(`use${storeName}Api must be used within <${storeName}Provider>.`);
35 }
36 return store;
37 }
38
39 return {
40 Provider,
41 useStore: useLocalStore,
42 useStoreApi: useLocalStoreApi,
43 };
44}使用起來非常直覺,分為三個步驟:定義、封裝、呼叫。
1import { createStore } from 'zustand/vanilla';
2
3interface CounterState {
4 count: number;
5 inc: () => void;
6}
7
8const { Provider, useStore, useStoreApi } = createLocalStoreContext(
9 'Counter',
10 () => createStore<CounterState>((set) => ({
11 count: 0,
12 inc: () => set((state) => ({ count: state.count + 1 })),
13 }))
14);
15
16export { Provider as CounterProvider, useStore as useCounterStore };1function CounterPage() {
2 return (
3 <CounterProvider>
4 <CounterDisplay />
5 <CounterButton />
6 </CounterProvider>
7 );
8}1function CounterDisplay() {
2 // 具備完美的 TypeScript 型別推導與 Selector 效能優點
3 const count = useCounterStore((s) => s.count);
4 return <h1>Count: {count}</h1>;
5}Provider 被卸載(Unmount)時,這個 Store 實例也會隨之被回收。你不需要手動呼叫 reset(),下次重新開啟頁面時,狀態永遠是初始值。useStore(store, selector),元件只會在選取的資料發生變動時重新渲染,維持了 Zustand 一貫的優異效能。displayName 與錯誤檢查,讓你在忘記包裹 Provider 時能立刻在控制台看到清晰的錯誤訊息,而不是看到 undefined 報錯。createLocalStoreContext 結合了 Zustand 的簡潔與 Context 的隔離性。如果你正在開發複雜的 React 應用,建議將這個工廠函式加入你的工具包中,這會讓你的元件狀態管理變得更加清晰且高品質。