在 React 開發中,狀態管理一直是一個熱門話題。從早期的 Redux 到現在的 Zustand、Jotai 等現代解決方案,開發者們總是在尋找最適合的狀態管理方式。但在選擇工具之前,我們需要先理解一個根本問題:為什麼 React 需要外部狀態管理?
React 初期的單向資料流設計
React 的設計哲學建立在單向資料流的基礎上。這意味著:
- 資料只能從父組件流向子組件
- 子組件不能直接修改父組件的狀態
- ❌ 不能這樣做:
props.count = 10直接修改 props 值 - ✅ 只能這樣做:通過回調函數
props.onIncrement()通知父組件更新
- ❌ 不能這樣做:
- 狀態更新必須通過 setState 或 useState
這種設計帶來了很多好處:
-
資料流向清晰可預測
- 例如:用戶登入狀態
App → Header → UserMenu,資料只會從上往下傳遞 - 狀態更新只會從子組件通過回調函數向上傳遞到父組件
- 例如:用戶登入狀態
-
組件間的依賴關係明確
- 例如:
UserMenu依賴Header傳遞的user和onLogoutprops - 可以清楚看到哪些組件需要哪些資料
- 例如:
-
容易進行除錯和測試
- 例如:測試
UserMenu時,只需要傳入{ user: mockUser, onLogout: mockFn } - 除錯時可以追蹤用戶登入狀態從哪個父組件傳入,登出回調函數會觸發哪個父組件的更新
- 例如:測試
// 典型的單向資料流 - 用戶登入情境
function App() {
const [user, setUser] = useState(null);
return (
<div>
<Header user={user} onLogout={() => setUser(null)} />
</div>
);
}
function Header({ user, onLogout }) {
return (
<header>
<UserMenu user={user} onLogout={onLogout} />
</header>
);
}
function UserMenu({ user, onLogout }) {
return (
<div>
{user ? (
<button onClick={onLogout}>登出 {user.name}</button>
) : (
<span>請先登入</span>
)}
</div>
);
}useState 與 props 傳遞的侷限
隨著應用程式變得複雜,單純使用 useState 和 props 傳遞會遇到以下問題:
1. 狀態分散在各個組件中
當每個組件都管理自己的狀態時,會導致:
- 難以追蹤:不知道某個狀態在哪個組件中
- 重複邏輯:多個組件可能有相似的狀態管理邏輯
- 同步困難:相關狀態分散在不同組件,難以保持一致性
// 每個組件都有自己的狀態 - 造成問題的範例
function App() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState("light");
const [notifications, setNotifications] = useState([]);
// 問題:不知道用戶偏好設定在哪個組件中
}
function Header() {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [user, setUser] = useState(null); // 重複:App 也有 user 狀態
// 問題:重複的用戶狀態管理邏輯
}
function Sidebar() {
const [isCollapsed, setIsCollapsed] = useState(false);
const [theme, setTheme] = useState("light"); // 重複:App 也有 theme 狀態
// 問題:主題狀態分散,難以保持同步
}
function UserMenu() {
const [user, setUser] = useState(null); // 又一個重複的 user 狀態
// 問題:三個組件都有 user,但可能不同步
}2. 狀態同步困難
當多個組件需要共享同一個狀態時,需要將狀態提升到共同的父組件,這會導致:
-
父組件變得臃腫:所有共享狀態都集中在父組件,導致組件過於龐大
- 例如:
App組件需要管理user、theme、notifications、cart、filters等 20+ 個狀態 - 結果:一個組件有 500+ 行代碼,難以閱讀和理解
- 例如:
-
狀態更新邏輯分散:同一個狀態的更新邏輯可能散落在多個子組件中
- 例如:
user狀態的更新邏輯在LoginForm、UserProfile、Header等 5 個組件中都有 - 結果:修改用戶登入邏輯需要同時修改多個文件
- 例如:
-
難以維護和測試:修改一個狀態需要同時考慮多個組件的影響
- 例如:你想把深色模式從
dark改成night,但不知道哪些組件在使用這個狀態 - 結果:改了
App組件的theme狀態,結果Header的按鈕顏色變了,Sidebar的背景沒變,Modal的邊框消失了 - 問題:一個簡單的改名,卻要修復 3 個不同的 bug
- 例如:你想把深色模式從
prop drilling 問題實例
Prop drilling 是指為了將資料傳遞到深層嵌套的組件,必須通過中間組件一層層傳遞 props 的問題。
// 經典的 prop drilling 範例
function App() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState("light");
return (
<div>
<Header user={user} theme={theme} />
<Main user={user} theme={theme} />
</div>
);
}
function Header({ user, theme }) {
return (
<header>
<Navigation user={user} theme={theme} />
</header>
);
}
function Navigation({ user, theme }) {
return (
<nav>
<UserMenu user={user} theme={theme} />
</nav>
);
}
function UserMenu({ user, theme }) {
return (
<div>
<Avatar user={user} />
<ThemeToggle theme={theme} />
</div>
);
}Prop drilling 的問題:
- 中間組件被迫接收不需要的 props
- 代碼變得冗長且難以閱讀
- 修改 props 結構需要更新多個組件
- 組件耦合度增加
Context API 的誕生與缺陷
為了解決 prop drilling 問題,React 16.3 引入了 Context API:
// 使用 Context API 解決 prop drilling
const UserContext = createContext();
const ThemeContext = createContext();
function App() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState("light");
return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value={{ theme, setTheme }}>
<Header />
<Main />
</ThemeContext.Provider>
</UserContext.Provider>
);
}
function UserMenu() {
const { user } = useContext(UserContext);
const { theme } = useContext(ThemeContext);
return (
<div>
<Avatar user={user} />
<ThemeToggle theme={theme} />
</div>
);
}Context API 的優點:
-
解決了 prop drilling 問題
- 例如:
UserMenu不再需要通過Header和Navigation傳遞user和onLogout - 結果:中間組件
Header和Navigation不再需要接收不需要的 props
- 例如:
-
組件可以直接存取需要的狀態
- 例如:
UserMenu可以直接使用useContext(UserContext)獲取用戶資料 - 結果:組件只依賴自己真正需要的資料,依賴關係更清晰
- 例如:
-
代碼更簡潔
- 例如:原本需要 3 層 props 傳遞,現在只需要 1 行
useContext - 結果:代碼行數減少,可讀性提升
- 例如:原本需要 3 層 props 傳遞,現在只需要 1 行
Context API 的缺陷:
-
性能問題:任何 Context 值改變都會重新渲染所有消費者
- 例如:
AppContext包含user、theme、notifications,當notifications更新時 - 結果:
Header、Sidebar、Main等所有使用 Context 的組件都會重新渲染 - 問題:即使組件只關心
user和theme,也會因為notifications更新而重新渲染
- 例如:
-
難以除錯:狀態更新來源不明確
- 例如:發現
user狀態變成null,但不知道是哪個組件調用了setUser(null) - 結果:需要在 10+ 個組件中搜尋
setUser的調用,才能找到問題源頭 - 問題:除錯時間大幅增加,容易遺漏某些調用點
- 例如:發現
-
缺乏時間旅行除錯:無法追蹤狀態變化歷史
- 例如:用戶報告「購物車突然清空了」,但你只能看到當前空的購物車狀態
- 結果:無法重現問題發生的過程,不知道是哪一步操作導致的
- 問題:無法回到問題發生前的狀態進行分析
時間旅行除錯的實際例子
時間旅行除錯是指能夠回到應用程式的任何一個歷史狀態,就像時光機一樣。讓我們看看具體的差異:
// Context API - 無法追蹤狀態變化歷史
const AppContext = createContext();
function App() {
const [count, setCount] = useState(0);
const [name, setName] = useState("");
// 當狀態改變時,你只能看到當前狀態
// 無法知道:count 是從 0 → 5 → 3 → 8 這樣變化的
const value = { count, setCount, name, setName };
return (
<AppContext.Provider value={value}>
<Counter />
<NameInput />
</AppContext.Provider>
);
}問題:如果用戶報告了一個 bug,你只能看到最終的狀態,無法重現問題發生的過程。
// 使用 Redux 時,可以追蹤每個狀態變化
const store = createStore(reducer);
// 用戶的操作會產生這樣的歷史記錄:
// Action 1: { type: 'INCREMENT', payload: 1 } → count: 1
// Action 2: { type: 'INCREMENT', payload: 1 } → count: 2
// Action 3: { type: 'SET_NAME', payload: 'John' } → name: 'John'
// Action 4: { type: 'INCREMENT', payload: 1 } → count: 3
// Action 5: { type: 'DECREMENT', payload: 1 } → count: 2實際使用場景:
- 重現 Bug:可以回到崩潰前的每一步,重現問題
- 理解複雜狀態變化:追蹤購物車、表單等複雜狀態的變化過程
- 測試不同操作順序:驗證不同操作順序是否會產生不同結果
// 現代狀態管理庫的解決方案
import { create } from "zustand";
import { devtools } from "zustand/middleware";
const useStore = create(
devtools(
(set) => ({
count: 0,
name: "",
increment: () => set((state) => ({ count: state.count + 1 })),
setName: (name) => set({ name }),
}),
{ name: "app-store" } // DevTools 中顯示的名稱
)
);
// 現在你可以在瀏覽器 DevTools 中看到:
// - 每個狀態變化的歷史
// - 觸發的 action 名稱
// - 狀態變化的時間戳
// - 可以回到任何歷史狀態// Context 性能問題範例
const AppContext = createContext();
function App() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState("light");
const [notifications, setNotifications] = useState([]);
// 任何一個狀態改變,所有消費者都會重新渲染
const value = {
user,
setUser,
theme,
setTheme,
notifications,
setNotifications,
};
return (
<AppContext.Provider value={value}>
<Header />
<Main />
<Sidebar />
</AppContext.Provider>
);
}狀態管理出現的根本原因
基於上述問題,外部狀態管理庫應運而生,它們解決了:
1. 狀態集中管理
// 使用 Zustand 集中管理狀態
import { create } from "zustand";
const useStore = create((set) => ({
user: null,
theme: "light",
notifications: [],
setUser: (user) => set({ user }),
setTheme: (theme) => set({ theme }),
addNotification: (notification) =>
set((state) => ({
notifications: [...state.notifications, notification],
})),
}));2. 性能優化
- 只有相關組件會在狀態改變時重新渲染
- 支援選擇性訂閱
- 內建 memoization
3. 開發者體驗
- 時間旅行除錯
- 狀態變化追蹤
- 更好的 TypeScript 支援
4. 可預測的狀態更新
- 明確的 action 和 reducer 模式
- 不可變的狀態更新
- 更容易測試和除錯
總結
React 的單向資料流設計雖然簡單直觀,但在複雜應用中會遇到 prop drilling 和狀態同步等問題。Context API 部分解決了這些問題,但在性能和開發者體驗方面仍有不足。
外部狀態管理庫的出現,正是為了解決這些根本性問題:
- 集中管理複雜的應用狀態
- 優化性能,避免不必要的重新渲染
- 提升開發者體驗,提供更好的除錯和測試工具
- 保持可預測性,讓狀態更新更加明確和可控