1. 何时需要全局状态
在 React 中,状态管理遵循单向数据流:数据从父组件向子组件流动(通过 Props),子组件通过回调函数通知父组件更改状态。这在组件层级较浅时运作良好,但随着应用变大,会出现问题。
Props Drilling 问题
假设有这样的组件树,App 中的 user 状态需要在深层的 Avatar 组件中使用:
// Props Drilling —— user 被迫经过所有中间组件
function App() {
const [user, setUser] = useState(currentUser);
return <Layout user={user} />; // Layout 不需要 user,但要传下去
}
function Layout({ user }: Props) {
return <Sidebar user={user} />; // Sidebar 也不需要,继续传
}
function Sidebar({ user }: Props) {
return <UserMenu user={user} />; // 还是要传
}
function UserMenu({ user }: Props) {
return <Avatar user={user} />; // 终于到达目的地
}
function Avatar({ user }: Props) {
return <img src={user.avatar} />; // 真正使用 user 的地方
}
中间的 Layout、Sidebar、UserMenu 根本不需要 user,却被迫接受并传递它。当 User 类型改变时,所有中间组件都要修改——这就是 Props Drilling(属性穿透)问题。
状态提升的极限
当多个不相关的组件需要共享同一状态时,需要把状态提升到它们最近的公共祖先。如果这个祖先是顶层 App,每次状态变化都会导致整个应用重渲染,既低效又难以维护。
何时考虑全局状态:当同一状态需要被相距较远的多个组件访问或修改,Props Drilling 层级超过 3-4 层,或者需要跨页面持久化状态时。
2. Context API 详解
React 内置的 Context API 让你绕过 Props,直接把数据注入任意深度的子组件。
createContext + Provider + useContext 三件套
import { createContext, useContext, useState, useCallback } from 'react';
// 步骤 1:创建 Context,指定类型和默认值
interface AuthContextType {
user: User | null;
isLoggedIn: boolean;
login(credentials: Credentials): Promise<void>;
logout(): void;
}
const AuthContext = createContext<AuthContextType | null>(null);
// 步骤 2:Provider 组件 —— 封装状态逻辑,提供给子树
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = useCallback(async (credentials: Credentials) => {
const u = await authService.login(credentials);
setUser(u);
}, []);
const logout = useCallback(() => setUser(null), []);
return (
<AuthContext.Provider value={{ user, isLoggedIn: !!user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
// 步骤 3:自定义 Hook 封装,避免每次都写 useContext
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth 必须在 AuthProvider 内使用');
return ctx;
}
// 在任意子组件中使用,无需 Props
function Header() {
const { user, logout } = useAuth();
return (
<header>
<span>欢迎, {user?.name}</span>
<button onClick={logout}>退出</button>
</header>
);
}
Context 的性能问题
这是 Context API 最大的坑:当 Provider 的 value 变化时,所有消费该 Context 的组件都会重渲染,不管它们实际使用的是哪部分数据。
// ❌ 问题:每次任何一个属性变化,所有消费者都重渲染
<AppContext.Provider
value={{ user, theme, language, notifications }}
>
// ✅ 解决方案1:拆分多个 Context(按变化频率分开)
<UserContext.Provider value={user}>
<ThemeContext.Provider value={{ theme, setTheme }}>
<NotificationContext.Provider value={notifications}>
{children}
</NotificationContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
// ✅ 解决方案2:用 useMemo 稳定 value
const value = useMemo(
() => ({ user, login, logout }),
[user, login, logout]
);
<AuthContext.Provider value={value}>
// ✅ 解决方案3:消费组件用 React.memo
const Avatar = memo(() => {
const { user } = useAuth();
return <img src={user?.avatar} />;
});
3. Zustand 介绍
Zustand(德语"状态")是目前最受欢迎的轻量级状态管理库之一。它的哲学是:简单、无模板代码、按需订阅。
与 Redux 的对比
| 特性 | Redux Toolkit | Zustand |
|---|---|---|
| 概念复杂度 | 高(store/slice/action/selector) | 低(create 一个函数搞定) |
| 样板代码 | 多(配置、combineReducers...) | 极少 |
| 包大小 | ~16kB(RTK) | ~1kB |
| TypeScript | 完整支持 | 完整支持,且更简洁 |
| DevTools | 官方 Redux DevTools | devtools 中间件 |
| 最优选择 | 大型团队、严格规范 | 中小型项目、快速开发 |
安装
npm install zustand
4. Zustand 基础使用
Zustand 的核心是 create 函数——传入一个函数,返回一个自定义 Hook。状态和更新它的 actions 都定义在同一个 create 调用中。
创建 Store
import { create } from 'zustand';
// 定义 store 的类型
interface CounterStore {
count: number;
increment(): void;
decrement(): void;
reset(): void;
incrementBy(n: number): void;
}
// create() 返回一个 Hook
const useCounterStore = create<CounterStore>()((set, get) => ({
count: 0,
// Actions:调用 set 更新状态
increment: () => set(state => ({ count: state.count + 1 })),
decrement: () => set(state => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
// get() 读取当前状态(不用于 UI,用于 action 内部逻辑)
incrementBy: (n) => set({ count: get().count + n }),
}));
在组件中使用
function Counter() {
// 只订阅需要的部分(Selector 优化)
const count = useCounterStore(state => state.count);
const increment = useCounterStore(state => state.increment);
const reset = useCounterStore(state => state.reset);
// count 变化时此组件重渲染;increment/reset 变化时不会(函数引用稳定)
return (
<div>
<p>{count}</p>
<button onClick={increment}>+</button>
<button onClick={reset}>重置</button>
</div>
);
}
// 也可以在组件外部直接调用 actions(不在组件里也能用!)
useCounterStore.getState().increment();
useCounterStore.setState({ count: 100 });
完整的购物车示例
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface CartStore {
items: CartItem[];
totalItems: number;
totalPrice: number;
addItem(item: Omit<CartItem, 'quantity'>): void;
removeItem(id: string): void;
updateQuantity(id: string, qty: number): void;
clearCart(): void;
}
const useCartStore = create<CartStore>()((set, get) => ({
items: [],
get totalItems() { return get().items.reduce((s, i) => s + i.quantity, 0); },
get totalPrice() { return get().items.reduce((s, i) => s + i.price * i.quantity, 0); },
addItem: (item) => set(state => {
const existing = state.items.find(i => i.id === item.id);
if (existing) {
return {
items: state.items.map(i =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
)
};
}
return { items: [...state.items, { ...item, quantity: 1 }] };
}),
removeItem: (id) => set(state => ({
items: state.items.filter(i => i.id !== id)
})),
updateQuantity: (id, qty) => set(state => ({
items: state.items.map(i => i.id === id ? { ...i, quantity: qty } : i)
})),
clearCart: () => set({ items: [] }),
}));
5. Zustand 进阶
Zustand 通过中间件(middleware)机制扩展功能,最常用的有 persist、devtools 和 immer。
persist 中间件 — 持久化到 localStorage
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
const useSettingsStore = create<SettingsStore>()(
persist(
(set) => ({
theme: 'dark',
language: 'zh',
fontSize: 14,
setTheme: (theme) => set({ theme }),
setLanguage: (lang) => set({ language: lang }),
}),
{
name: 'app-settings', // localStorage key
storage: createJSONStorage(() => localStorage),
// 只持久化部分字段(跳过 actions)
partialize: (state) => ({
theme: state.theme,
language: state.language,
}),
}
)
);
devtools 中间件 — Redux DevTools 支持
import { devtools } from 'zustand/middleware';
const useStore = create<Store>()(
devtools(
(set) => ({
count: 0,
// 第二个参数是 action 名称,在 DevTools 中显示
increment: () => set(s => ({ count: s.count + 1 }), false, 'counter/increment'),
}),
{ name: 'MyApp Store' }
)
);
immer 中间件 — 可变语法写不可变更新
import { immer } from 'zustand/middleware/immer';
const useStore = create<Store>()(
immer((set) => ({
user: { name: 'Alice', address: { city: 'Beijing' } },
skills: ['JS', 'TS'],
// immer 让你用"可变"语法写,内部自动生成不可变更新
updateCity: (city) => set(state => {
state.user.address.city = city; // 看起来在直接修改,实际上是安全的
}),
addSkill: (skill) => set(state => {
state.skills.push(skill); // 可以直接 push!
}),
}))
);
// 组合多个中间件
const useStore = create<Store>()(
devtools(persist(immer((set) => ({ /* ... */ }))))
);
6. 状态设计原则
最小化状态(Minimal State)
只把真正需要的数据放进 state,其余可以从 state 派生(计算)出来的数据不要重复存储。
// ❌ 冗余状态 —— totalPrice 可以从 items 计算出来
const [items, setItems] = useState([]);
const [totalPrice, setTotalPrice] = useState(0); // 重复!
// ✅ 最小化 state,派生值用 useMemo 计算
const [items, setItems] = useState([]);
const totalPrice = useMemo(
() => items.reduce((sum, item) => sum + item.price * item.qty, 0),
[items]
);
状态归属原则
把状态放在需要它的最低层级的组件中("状态下推")。不是所有状态都需要全局管理。
- 本地状态(useState):只影响当前组件,如表单输入、下拉菜单开关、临时 UI 状态
- Context:主题、语言、当前登录用户——跨组件共享但变化不频繁
- Zustand / Redux:业务核心数据——购物车、用户权限、全局通知队列等
单一数据源(Single Source of Truth)
同一份数据只在一个地方维护。如果用户信息存在 Context 里,就不要同时存在 Zustand store 里——保持同步两份数据是灾难的根源。
7. 状态管理方案全面对比
根据实际场景选择合适的状态管理工具,没有"最好的",只有"最合适的"。
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
useState |
组件内部状态 表单、UI 开关 |
零配置,最简单 | 无法跨组件共享 |
useReducer |
复杂组件内状态 多步骤表单 |
逻辑集中,可预测 | 仍是组件内状态 |
Context API |
主题/语言/用户信息 变化不频繁的共享数据 |
内置,无依赖 | 性能问题,全量重渲染 |
| Zustand | 中小型应用全局状态 业务数据(购物车等) |
极简 API,按需订阅 性能优秀 |
无强制规范,大团队需约定 |
| Redux Toolkit | 大型企业应用 多人协作,严格规范 |
生态完整,可预测 时间旅行调试 |
样板代码多,学习曲线陡 |
决策树
这个状态只有当前组件用?
→ 是:useState / useReducer
多个组件共享,但变化不频繁(主题/语言)?
→ 是:Context API
业务核心数据,频繁更新,多组件读写?
→ Zustand(推荐)或 Redux Toolkit
需要严格流程规范、时间旅行调试、大团队协作?
→ Redux Toolkit
Zustand 最佳实践总结
// ✅ 推荐的 Store 组织方式 —— 按功能拆分多个 store
// stores/useUserStore.ts
export const useUserStore = create<UserStore>()(...);
// stores/useCartStore.ts
export const useCartStore = create<CartStore>()(...);
// ✅ Selector 精确订阅,避免不必要重渲染
const username = useUserStore(state => state.user?.name);
const itemCount = useCartStore(state => state.items.length);
// ✅ Actions 定义在 store 内,而不是组件里
const addToCart = useCartStore(state => state.addItem);
// ✅ 复杂 selector 用 useShallow 批量订阅(避免每次都返回新对象)
import { useShallow } from 'zustand/react/shallow';
const { theme, language } = useSettingsStore(
useShallow(state => ({ theme: state.theme, language: state.language }))
);
本章小结:状态管理的核心是"把对的数据放在对的地方"。从 useState 开始,Props Drilling 严重时引入 Context,业务数据复杂时用 Zustand。Zustand 的 persist 中间件轻松实现本地持久化,devtools 中间件方便调试。下一章我们进入路由管理。