第八章:當 useState 已經撐不住了 - Zustand 狀態管理大師
當你的後台系統只有一兩個頁面時,我們在第一章學過的 useState 絕對夠用。
但隨著系統越來越龐大,你可能會遇到一個大麻煩:
假設你在首頁 (Dashboard) 呼叫了打卡 API,取得了一個名為 punchRecords 的陣列。
結果你的「圖表區塊 (Chart)」需要這個陣列來畫圖,你的「資料表區塊 (DataTable)」需要這個陣列來顯示名單,你的「側邊欄 (Sidebar)」需要這個陣列來計算今天有幾個人遲到。
如果你只用 useState,你會發現你必須把這個 punchRecords 像接力賽的棒子一樣,從最上層的父元件,一層一層透過 props 往下傳遞給所有的子元件。
這種現象在業界被稱為 「Prop Drilling (屬性鑽孔)」。這會讓你的程式碼變成一團亂麻,而且只要中間有一層沒傳好,整個系統就壞了!
為了解決這個問題,我們需要「全域狀態管理 (Global State Management)」。
🎯 本章目標
- 聽懂全域狀態管理的概念:什麼是 Store (倉庫)?
- 認識目前 React 社群最推崇、最輕量的狀態套件:Zustand。
- 建立一個全域的
userStore來管理登入者的資訊。 - 學習如何在完全不相關的兩個元件中,讀寫同一個全域資料。
🐻 為什麼選 Zustand,而不是老牌的 Redux?
如果你去查五年前的 React 教學,他們一定會教你用 Redux。
Redux 雖然強大,但它需要寫幾十行的樣板程式碼 (Boilerplate) 才能存一個小小的變數,而且充滿了像是 Reducer, Dispatch, Action 這種反人類的專有名詞。
Zustand (德文的「狀態」之意) 是一隻可愛的熊。它的設計理念是:「越簡單越好」。 它幫你在雲端開了一個「共用大倉庫 (Store)」,任何元件只要拿到對應的鑰匙 (Hook),就能直接去倉庫拿資料或是改資料,完全不需要透過父元件傳遞!
📦 第一步:請 AI 幫你開倉庫
🔥【Vibe Prompt 實戰咒語】
我需要在 React (Vite) 中使用 Zustand 作為全域狀態管理。1. 請給我 npm 安裝指令。2. 請幫我在 src/store/ 建立一個 userStore.ts。3. 這個 store 需要包含 userInfo (這是一個物件,包含 name, role, avatar 欄位,預設為 null)。4. 請提供兩個更新狀態的函數:一個是 setUserInfo (登入時寫入資料),另一個是 clearUser (登出時清空資料)。5. 請加上 TypeScript 的 Type 定義,讓我的編輯器會有自動提示。
AI 會教你打開終端機安裝:
npm install zustand
並生成這個極度優雅的倉庫設定檔:
// 📂 src/store/userStore.ts
import { create } from 'zustand';
// 1. 定義倉庫裡面有哪些東西 (TypeScript 型別定義)
interface UserInfo {
name: string;
role: string;
avatar?: string;
}
interface UserState {
userInfo: UserInfo | null;
setUserInfo: (user: UserInfo) => void;
clearUser: () => void;
}
// 2. 建立並開放這座大倉庫
export const useUserStore = create<UserState>((set) => ({
userInfo: null, // 剛打開網頁時,預設沒有人登入
// 寫入資料的魔法:呼叫 set() 函數來更新倉庫
setUserInfo: (user) => set({ userInfo: user }),
// 清空資料的魔法
clearUser: () => set({ userInfo: null }),
}));
🚀 第二步:在任何角落「隨取隨用」
現在,這座倉庫已經飄在你的網站上空。你可以在網站的任何一個角落拿資料!
情境 A:在導覽列 (Navbar.tsx) 中讀取名字
import { useUserStore } from '../store/userStore';
export default function Navbar() {
// 直接呼叫我們剛剛做的 Hook,告訴它「我只要拿 userInfo 這個東西」
const userInfo = useUserStore((state) => state.userInfo);
return (
<nav className="bg-white p-4 flex justify-between shadow-sm">
<div className="font-bold">打卡系統管理後台</div>
{/* 判斷有沒有登入,來顯示不同的文字 */}
<div className="text-blue-600">
你好,{userInfo ? userInfo.name : '訪客'}
</div>
</nav>
);
}
情境 B:在登入頁 (Login.tsx) 中寫入資料
import { useUserStore } from '../store/userStore';
export default function Login() {
// 這次我們告訴倉庫:「我需要拿到 setUserInfo 這個寫入函數」
const setUserInfo = useUserStore((state) => state.setUserInfo);
const handleFakeLogin = () => {
// 假設打完 API 登入成功,直接把資料存進全域倉庫!
setUserInfo({ name: '王大老闆', role: 'admin' });
alert('登入成功!請看上方的 Navbar 名字有沒有改變?');
};
return (
<button onClick={handleFakeLogin} className="bg-blue-500 text-white p-2 rounded">
模擬登入
</button>
);
}
你會發現,Navbar 和 Login 是完全獨立的兩個元件,但只要 Login 一執行 setUserInfo,Navbar 上面的字就會「瞬間」變成王大老闆!這就是全域狀態的強大威力。
💼 [商業應用場景] 什麼東西該放進 Zustand?
新手學會 Zustand 後常犯的錯誤,就是把所有的變數 (包含輸入框打到一半的字、彈出視窗的開關) 全都塞進 Zustand。這會導致記憶體肥大,且非常難以維護。
記住一個商業原則:「只有跨頁面、跨元件都需要用到的共享資料,才放進 Zustand。」
- ✅ 適合放:登入者資訊、全站的深色/淺色模式、購物車裡的商品數量。
- ❌ 不適合放:這個頁面專屬的 Data Table 搜尋關鍵字、某個按鈕點下去是否跳出確認框 (這種用
useState就好)。
✅ 本章小結
掌握了 Zustand,你的 React 專案架構能力就真正脫離了「新手村」。 未來不管你要做多複雜的電子商務系統,只要把跨頁面的資料抽離出來放到 Zustand 裡面,你的元件樹就會變得異常乾淨清爽。這也是你在開發大型企業專案時,保持不崩潰的最強護城河!