状态管理不是选库,是设计数据流。
从useState开始,到你真正需要外部库时才引入它。
技术介绍
React 组件的一切变化都源于状态。
状态管理要解决三件事:数据存在哪、谁来改、改了怎么通知视图。
React 本身提供了 useState、useReducer、useContext 三板斧。
当应用规模膨胀,出现“跨层级共享”“复杂更新逻辑”“性能瓶颈”时,才需要引入外部方案(如 Redux Toolkit、Zustand)。
掌握原则比背 API 重要:让状态尽可能局部,提升时别怕麻烦。
必会基础
useState
函数组件的记忆单元。 让你在多次渲染之间保留一个值,并且值变化时触发重新渲染。
const [count, setCount] = useState(0);
// count:当前值 setCount:更新函数
// 基本用法:计数器
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
点击了 {count} 次
</button>
);
}
// 函数式更新:避免闭包陷阱
function DelayedCounter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
// 用函数形式拿到最新 state,而不是过期的闭包值
setCount(prev => prev + 1);
}, 1000);
};
return <button onClick={handleClick}>延迟+1:{count}</button>;
}
典型场景
- 表单输入、开关、计数器等组件内部状态
- 一切会触发 UI 变化的变量
边界提醒
- 不要在循环或条件语句中调用
useState(会打乱 hooks 调用顺序) - 更新函数是异步的,紧挨着读取
count可能还是旧值——需要新值用prev
useReducer
当“下一个状态依赖前一个状态”,且逻辑复杂时,用 reducer 比多个 useState 更清晰。
它把状态更新收拢到一个纯函数里。
const [state, dispatch] = useReducer(reducer, initialState);
// 购物车状态管理
const cartReducer = (state, action) => {
switch (action.type) {
case 'ADD':
return [...state, action.item];
case 'REMOVE':
return state.filter(item => item.id !== action.id);
case 'CLEAR':
return [];
default:
return state;
}
};
function ShoppingCart() {
const [cart, dispatch] = useReducer(cartReducer, []);
return (
<div>
<button onClick={() => dispatch({ type: 'ADD', item: { id: 1, name: '书' } })}>
添加
</button>
<button onClick={() => dispatch({ type: 'CLEAR' })}>
清空
</button>
<ul>
{cart.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</div>
);
}
典型场景
- 表单多字段联动校验
- 购物车、多步骤向导等状态互相依赖的场景
- 一个操作会同时修改多个状态值
边界提醒
- 别把
useReducer当成全局状态管理——它仍是组件私有 - reducer 必须是纯函数:同样的 state 和 action 永远返回同样的新 state
- 不要在里面做副作用(请求、定时器)
状态提升(Lifting State Up)
两个兄弟组件需要共享状态时,把状态移到它们最近的公共父组件。
这是 React 最基础的数据共享手段。
function Parent() {
const [activeId, setActiveId] = useState(null);
return (
<div>
<ItemList activeId={activeId} onSelect={setActiveId} />
<ItemDetail id={activeId} />
</div>
);
}
典型场景
- 选项卡切换,列表和详情联动
- 受控输入组件的值需要被外部知道
边界提醒
- 提升不是越高越好:升得太高会导致无意义的重渲染,且 props 传递路径过长
- 一旦你觉得 props 开始“层层传递但中间组件不用”,就该考虑组合或 Context
核心原理
状态快照与闭包
每次渲染都有自己独立的 state 和 props(快照)。
事件处理函数会“记住”那次渲染时的值,这就是闭包陷阱的来源。
function AlertBug() {
const [count, setCount] = useState(0);
const showCount = () => {
// 点击时创建的闭包捕获了当时的 count
setTimeout(() => alert(count), 2000);
};
return (
<div>
<p>当前计数:{count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<button onClick={showCount}>2秒后弹出count</button>
</div>
);
}
// 快速点 +1 三次再点弹窗,弹出的仍是点击弹窗时的值
如何预判
- 在异步操作里读取 state,一定问自己:这个值来自哪次渲染?
- 用函数式更新或
useRef来穿透闭包
渲染触发条件
状态变化(引用变化)触发重新渲染,父组件渲染也会导致子组件渲染。
理解这一点才能做性能优化。
useState的 setter 使用Object.is比较,如果值相同则跳过- 父组件重渲染 → 子组件默认也跟着重渲染(即使 props 没变)
典型误判:以为改了对象内部属性就能触发渲染——你必须传新对象引用。
const [user, setUser] = useState({ name: 'Tom', age: 18 });
// ❌ 错误:直接修改后 setUser 同一个对象,不会重渲染
user.age = 19;
setUser(user);
// ✅ 正确:创建新对象
setUser({ ...user, age: 19 });
Redux 数据流与不可变性(原理衔接)
Redux 是基于 单向数据流 的状态管理库,它强制:
- 状态是单一不可变树(store)
- 改变状态的唯一方式是派发(dispatch)一个动作(action)
- 动作被纯函数(reducer)处理,返回全新的状态对象
这种“描述变化”而非“直接修改”的模式让状态变更可预测、可回溯,但也引入了样板代码。
Redux Toolkit 用 createSlice 内部集成 Immer,让你可以用“变异”写法,但 Immer 会在背后生成不可变更新。
关键心智:Redux 将组件状态的更新机制提取到组件外部统一管理,因此特别适合大型应用的多团队协作。
实战模式
受控表单通用处理(useState)
用一个 useState 对象承载表单,用 name 属性驱动统一更新。
function Form() {
const [form, setForm] = useState({ username: '', email: '' });
const handleChange = (e) => {
const { name, value } = e.target;
setForm(prev => ({ ...prev, [name]: value }));
};
return (
<>
<input name="username" value={form.username} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />
</>
);
}
为什么这么写:避免每个字段写一个 useState,方便后续校验和提交。
跨层级共享(Context + useReducer)
当数据和修改逻辑都需要跨组件传递时,用 Context 包裹 dispatch 和 state。
const CountContext = createContext();
function CountProvider({ children }) {
const [count, dispatch] = useReducer(countReducer, 0);
return (
<CountContext.Provider value={{ count, dispatch }}>
{children}
</CountContext.Provider>
);
}
// 任意子组件
function DeepChild() {
const { count, dispatch } = useContext(CountContext);
return <button onClick={() => dispatch({ type: 'inc' })}>{count}</button>;
}
典型场景:应用级的用户信息、主题、权限数据。
边界提醒
- Context 值变化会重新渲染所有用到该 Context 的消费者(其下子树)
- 不要把频繁变化的值(如每秒变化的时间)直接放 Context
Redux Toolkit(企业级状态管理)
Redux 的官方工具集,大幅简化 Redux 的配置和代码量。核心三要素:Store、Slice、组件连接。
Redux Store 与 Slice (createSlice + configureStore)
一个 Slice 就是一个独立的状态片段及其更新逻辑,Store 是它们的集合。
import { configureStore, createSlice } from '@reduxjs/toolkit';
// 创建 Slice:自治的状态 + reducer + action 生成
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; }, // 可“直接修改”,Immer 保证不可变
decrement: (state) => { state.value -= 1; },
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});
// 导出 action 创建函数
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
// 配置 store,将 slice 的 reducer 挂载
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
},
});
典型场景
- 需要全局共享的复杂状态,如用户信息、购物车、多页面表单数据
- 开发大型项目需要状态变更的可视化(Redux DevTools 开箱即用)
边界提醒
- 不要把所有状态都塞进 Redux:表单临时值、UI 开关等保留在组件本地更合适
- Slice 的 reducer 函数里不能有副作用(异步请求等),需借助
createAsyncThunk或中间件
组件连接 Redux (useSelector / useDispatch)
组件通过 hooks 读取状态和派发动作,不再需要 connect 高阶组件。
import { useSelector, useDispatch } from 'react-redux';
import { increment, incrementByAmount } from './counterSlice';
function Counter() {
const count = useSelector((state) => state.counter.value); // 精确订阅需要的部分
const dispatch = useDispatch();
return (
<div>
<span>{count}</span>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
</div>
);
}
典型场景
- 任何需要访问或修改全局状态的组件
useSelector的第二个参数可传入比较函数(如shallowEqual)来避免不必要重渲染
边界提醒
useSelector返回整个对象字面量时,每次 store 更新都会返回新引用,导致组件重渲染- 解决:分别取出原始值,或用
shallowEqual比较
处理异步 (createAsyncThunk)
将异步逻辑从组件中剥离,像定义同步 action 一样定义异步操作。
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
// 定义异步 thunk
export const fetchUserById = createAsyncThunk(
'user/fetchById',
async (userId) => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
);
const userSlice = createSlice({
name: 'user',
initialState: { entity: null, status: 'idle' },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUserById.pending, (state) => { state.status = 'loading'; })
.addCase(fetchUserById.fulfilled, (state, action) => {
state.status = 'succeeded';
state.entity = action.payload;
})
.addCase(fetchUserById.rejected, (state) => { state.status = 'failed'; });
},
});
// 组件中直接 dispatch 这个 thunk,就像 dispatch 普通 action
dispatch(fetchUserById(123));
典型场景
- 数据获取、提交表单等任何需要与后端交互的状态过程
- 需要全局管理请求的 loading / error 状态
边界提醒
- thunk 内部不要直接访问 store,通过
getState参数可控读取 - 简单的一次性请求(如登录表单)有时用组件内
useState+useEffect更直接,不必上 Redux
轻量状态库(以 Zustand 为例)
当状态需要被多个无关组件共享,且追求极简配置时,引入库。
import { create } from 'zustand';
const useBearStore = create((set) => ({
bears: 0,
increase: () => set((state) => ({ bears: state.bears + 1 })),
removeAll: () => set({ bears: 0 }),
}));
function Controls() {
const increase = useBearStore((s) => s.increase);
return <button onClick={increase}>加一头熊</button>;
}
选择时机:大量 props 穿梭、不相关的 UI 区域需要访问同一份数据,但不想要 Redux 的范式约束。
进阶与避坑
状态拆分 vs 合并
一个大的对象状态会导致无关组件重渲染,拆分成多个细粒度 state 是免费的性能优化。
// ❌ 一个对象,改 name 会导致 AgeDisplay 也重渲染
const [person, setPerson] = useState({ name: 'Tom', age: 20 });
// ✅ 拆开,改 name 只有 NameDisplay 重渲染
const [name, setName] = useState('Tom');
const [age, setAge] = useState(20);
权衡:如果这些值总是一起更新,合并反而更清晰,不用提前优化。
避免在渲染中设置状态
渲染期间调用 setState 会导致“太多重渲染”错误,并产生不可预测的结果。
// ❌ 致命错误
function Bad() {
const [x, setX] = useState(0);
if (x < 10) setX(x + 1); // 渲染中更新状态
return <div>{x}</div>;
}
唯一例外:通过 useEffect 或事件处理程序来更新,但记得正确设置依赖。
状态的不可变性(含 Redux Toolkit 的 Immer 陷阱)
React 和 Redux 都依赖引用比较判断更新。始终返回新对象、新数组,不要原地修改。
// 数组添加
setList(prev => [...prev, newItem]); // 新数组
// 数组删除
setList(prev => prev.filter(item => item.id !== id));
// 数组修改某一项
setList(prev => prev.map(item => item.id === id ? {...item, done: true} : item));
Redux Toolkit 注意:
createSlice 的 reducer 里你可以写 state.value = 10,这是 Immer 的魔法。
但如果脱离 createSlice(比如在 thunk 里手动更新),或把状态传给外部函数,必须保持不可变操作。
另外,console.log(state) 在 Immer 代理下可能看到 Proxy 对象,用 current(state) 打印原始内容。
Redux Selector 性能(避免无意义渲染)
useSelector 的默认比较是 ===,只要引用变化就重渲染。从 store 返回组合对象会每次创建新引用。
// ❌ 每次都返回新对象,组件一定重渲染
const { name, age } = useSelector(state => ({
name: state.user.name,
age: state.user.age,
}));
// ✅ 分别选取基本值(引用稳定)
const name = useSelector(state => state.user.name);
const age = useSelector(state => state.user.age);
// ✅ 或者使用 shallowEqual
import { shallowEqual } from 'react-redux';
const user = useSelector(state => state.user, shallowEqual);
进阶方案:用 createSelector 创建记忆化 selector,避免派生数据的重复计算。
import { createSelector } from '@reduxjs/toolkit';
const selectUser = state => state.user;
const selectName = createSelector(selectUser, user => user.name);
// 只有 user 变化时 selectName 才重新计算,引用也保持稳定
惰性初始化
当初始状态计算开销大时,传入一个函数,它只在首次渲染执行一次。
// ❌ 每次渲染都会调用 expensiveCalc(虽然只有首次的值被使用)
const [data] = useState(expensiveCalc());
// ✅ 惰性初始化
const [data] = useState(() => expensiveCalc());
明确场景:读取 localStorage、复杂数据转换等。
提防无意义的重渲染
通过将值稳定的对象提到组件外部,或使用 useMemo、useRef 固定引用,减少子组件无效更新。
// 将这个常量定义在组件外面,因为它的引用永不变
const OPTIONS = [{ label: 'A', value: 1 }];
function Stable() {
// 或者用 useRef 存不需要触发渲染的值
const timerRef = useRef(null);
// ...
}
实用心法:如果你传给子组件的 props 里包含内联对象/函数,子组件多半在受不必要的苦。
Redux 与 Context 的选择
| 场景 | 推荐 |
|---|---|
| 少数组件间的共享状态 | 状态提升或 Context |
| 全局状态,更新频率低(如主题、用户信息) | Context + useReducer |
| 高频更新、复杂更新逻辑、需要中间件/调试工具 | Redux Toolkit |
| 中小项目追求极简 | Zustand |
常见错误:用 Redux 管理所有状态,连表单单次输入的值都放进 store,这会让应用变慢且难以维护。
状态管理的尽头,是合理的组件设计。
先把组件树和数据流理清楚,库只是最后的胶水。