第八章:當 useState 已經撐不住了 - Zustand 狀態管理大師

當你的後台系統只有一兩個頁面時,我們在第一章學過的 useState 絕對夠用。 但隨著系統越來越龐大,你可能會遇到一個大麻煩:

假設你在首頁 (Dashboard) 呼叫了打卡 API,取得了一個名為 punchRecords 的陣列。 結果你的「圖表區塊 (Chart)」需要這個陣列來畫圖,你的「資料表區塊 (DataTable)」需要這個陣列來顯示名單,你的「側邊欄 (Sidebar)」需要這個陣列來計算今天有幾個人遲到。

如果你只用 useState,你會發現你必須把這個 punchRecords 像接力賽的棒子一樣,從最上層的父元件,一層一層透過 props 往下傳遞給所有的子元件。 這種現象在業界被稱為 「Prop Drilling (屬性鑽孔)」。這會讓你的程式碼變成一團亂麻,而且只要中間有一層沒傳好,整個系統就壞了!

為了解決這個問題,我們需要「全域狀態管理 (Global State Management)」。

🎯 本章目標

  1. 聽懂全域狀態管理的概念:什麼是 Store (倉庫)?
  2. 認識目前 React 社群最推崇、最輕量的狀態套件:Zustand
  3. 建立一個全域的 userStore 來管理登入者的資訊。
  4. 學習如何在完全不相關的兩個元件中,讀寫同一個全域資料。

🐻 為什麼選 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>
  );
}

你會發現,NavbarLogin 是完全獨立的兩個元件,但只要 Login 一執行 setUserInfoNavbar 上面的字就會「瞬間」變成王大老闆!這就是全域狀態的強大威力。

💼 [商業應用場景] 什麼東西該放進 Zustand?

新手學會 Zustand 後常犯的錯誤,就是把所有的變數 (包含輸入框打到一半的字、彈出視窗的開關) 全都塞進 Zustand。這會導致記憶體肥大,且非常難以維護。

記住一個商業原則:「只有跨頁面、跨元件都需要用到的共享資料,才放進 Zustand。」

  • ✅ 適合放:登入者資訊、全站的深色/淺色模式、購物車裡的商品數量。
  • ❌ 不適合放:這個頁面專屬的 Data Table 搜尋關鍵字、某個按鈕點下去是否跳出確認框 (這種用 useState 就好)。

✅ 本章小結

掌握了 Zustand,你的 React 專案架構能力就真正脫離了「新手村」。 未來不管你要做多複雜的電子商務系統,只要把跨頁面的資料抽離出來放到 Zustand 裡面,你的元件樹就會變得異常乾淨清爽。這也是你在開發大型企業專案時,保持不崩潰的最強護城河!

解鎖完整教學內容

本章為付費內容。加入專案即可解鎖超過 5000 字的深度解析,包含 10 個以上神級 Prompt 與真實 Source Code 範例!