在大型 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 的建立與銷毀跟著元件生命週期走。
'use client';
import { createContext, useContext, useState, type ReactNode } from 'react';
import { useStore, type StoreApi } from 'zustand';
export function createLocalStoreContext<TStore>(
storeName: string,
createStoreFn: () => StoreApi<TStore>,
) {
const StoreContext = createContext<StoreApi<TStore> | null>(null);
function Provider({ children }: { children: ReactNode }) {
// 使用 useState lazy initializer 確保只在掛載時建立一次 store
const [store] = useState(() => createStoreFn());
return (
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
);
}
Provider.displayName = `${storeName}Provider`;
function useLocalStore<T>(selector: (state: TStore) => T): T {
const store = useContext(StoreContext);
if (!store) {
throw new Error(`use${storeName} must be used within <${storeName}Provider>.`);
}
return useStore(store, selector);
}
function useLocalStoreApi(): StoreApi<TStore> {
const store = useContext(StoreContext);
if (!store) {
throw new Error(`use${storeName}Api must be used within <${storeName}Provider>.`);
}
return store;
}
return {
Provider,
useStore: useLocalStore,
useStoreApi: useLocalStoreApi,
};
}使用起來非常直覺,分為三個步驟:定義、封裝、呼叫。
import { createStore } from 'zustand/vanilla';
interface CounterState {
count: number;
inc: () => void;
}
const { Provider, useStore, useStoreApi } = createLocalStoreContext(
'Counter',
() => createStore<CounterState>((set) => ({
count: 0,
inc: () => set((state) => ({ count: state.count + 1 })),
}))
);
export { Provider as CounterProvider, useStore as useCounterStore };function CounterPage() {
return (
<CounterProvider>
<CounterDisplay />
<CounterButton />
</CounterProvider>
);
}function CounterDisplay() {
// 具備完美的 TypeScript 型別推導與 Selector 效能優點
const count = useCounterStore((s) => s.count);
return <h1>Count: {count}</h1>;
}Provider 被卸載(Unmount)時,這個 Store 實例也會隨之被回收。你不需要手動呼叫 reset(),下次重新開啟頁面時,狀態永遠是初始值。useStore(store, selector),元件只會在選取的資料發生變動時重新渲染,維持了 Zustand 一貫的優異效能。displayName 與錯誤檢查,讓你在忘記包裹 Provider 時能立刻在控制台看到清晰的錯誤訊息,而不是看到 undefined 報錯。createLocalStoreContext 結合了 Zustand 的簡潔與 Context 的隔離性。如果你正在開發複雜的 React 應用,建議將這個工廠函式加入你的工具包中,這會讓你的元件狀態管理變得更加清晰且高品質。