9.8 KiB
tags, create time
| tags | create time | |||||
|---|---|---|---|---|---|---|
|
2026-04-29 22:03 |
State 与不可变性
概述
State 是 React 组件的数据源。不可变性(Immutability)是 React 状态管理的基石——React 通过引用比较判断 state 是否变化,只有创建了新的引用才会触发重渲染。理解这一点,就能避免 "数据变了但 UI 没更新" 这一经典 Bug。
为什么必须遵守不可变性?
graph LR
A["setState(newState)"] --> B{"old === new?"}
B -->|不同| C["触发重渲染 ✅"]
B -->|相同| D["跳过渲染 ❌ 数据已变但 UI 不变"]
style C fill:#4FC08D,color:#fff
style D fill:#F5A87D,color:#000
[!question] 思考:为什么 React 用浅比较而不是深比较?
- 性能:深比较复杂度为 O(n),大型嵌套对象代价极高;浅比较 O(1)
- 职责划分:React 负责高效检测变化,开发者负责正确生成新引用
- 历史教训:Redux / MobX 等库的对比证明,强制不可变性让调试和追踪变更成为可能
[!note] 核心原则 React 只认引用地址,不认内容。
{a: 1}和{a: 1}在 JS 中是两个不同的对象,===为false。因此只要每次创建新引用,React 就会检测到变化。
useState 基础用法
const [count, setCount] = useState(0);
const [user, setUser] = useState<{ name: string; age: number }>({ name: "", age: 0 });
const [items, setItems] = useState<string[]>([]);
// setState 可以接收值或 updater 函数
setCount(c => c + 1); // 推荐:基于旧值时用 updater,避免闭包陷阱
Updater 函数模式
当新 state 依赖旧 state 时,必须使用 updater 形式:
// 场景:快速连续点击,用值可能导致丢失更新
handleClick = () => {
setCount(count + 1); // ❌ 如果 count 被缓存,可能只+1
setCount(count + 1);
};
handleClick = () => {
setCount(c => c + 1); // ✅ 每次都拿到最新值,两次点击正确 +2
setCount(c => c + 1);
};
[!tip] 最佳实践 养成习惯:凡是新值依赖于旧值,一律用 updater 函数。即使当前看起来没问题,也能预防未来重构引入的闭包问题。
不可变更新模式
数组操作
const [list, setList] = useState(["a", "b", "c"]);
// 添加末尾
setList(prev => [...prev, "d"]);
// 插入开头
setList(prev => ["new", ...prev]);
// 删除(按 index)
setList(prev => prev.filter((_, i) => i !== targetIndex));
// 更新指定元素
setList(prev => prev.map((item, i) =>
i === targetIndex ? { ...item, completed: true } : item
));
// 安全排序(sort() 原地修改!必须先拷贝)
setList(prev => [...prev].sort((a, b) => a.id - b.id));
[!warning] 常见陷阱
// ❌ sort()/reverse()/push()/splice() 都原地修改原数组! list.sort(); // list 引用不变 setList(list); // React 跳过渲染 // ✅ 先拷贝再操作 setList([...list].sort());
对象更新
const [form, setForm] = useState({
profile: { name: "", email: "" },
settings: { theme: "light", notifications: true },
});
// 顶层字段 —— 展开一层即可
setForm(prev => ({ ...prev, title: "新标题" }));
// 嵌套一层 —— 逐层展开
setForm(prev => ({
...prev,
profile: { ...prev.profile, name: "新名字" },
}));
[!warning] 多层嵌套会非常冗长,这是手动管理复杂 state 的天然缺陷
如果你的嵌套超过两层,说明应该考虑
useReducer或外部状态管理库了。
状态提升
[!abstract] 什么是状态提升? React 官方提出的设计模式:将共享状态移动到最先需要它的共同父组件中,子组件通过 props 读取,通过回调 prop 请求变更。
flowchart TD
subgraph A["状态提升前 ❌"]
S1[CounterA 独立 state]
S2[CounterB 独立 state]
S3[总和无法计算]
S1 --> S3
S2 --> S3
end
subgraph B["状态提升后 ✅"]
P[App 持有 state]
C1[CounterA via props]
C2[CounterB via props]
Total["显示总和"]
P --> C1
P --> C2
P --> Total
end
// 状态提升后
function App() {
const [countA, setCountA] = useState(0);
const [countB, setCountB] = useState(0);
return (
<>
<p>总和: {countA + countB}</p>
<Counter value={countA} onChange={setCountA} />
<Counter value={countB} onChange={setCountB} />
</>
);
}
[!info] 何时使用状态提升?
- 两个或多个组件需要同步同一份数据
- 一个组件的交互需要影响其他组件的行为
- 没有更好的替代方案(如 Context、状态管理库)之前,先从状态提升开始
useReducer:管理复杂 state
当 state 逻辑变复杂时,useReducer 比 useState(updater) 更清晰:
interface State {
items: Item[];
status: "idle" | "loading" | "error";
error?: string;
}
type Action =
| { type: "ADD"; payload: Item }
| { type: "SET_LOADING" }
| { type: "SET_ERROR"; payload: string };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "ADD": return { ...state, items: [...state.items, action.payload] };
case "SET_LOADING": return { ...state, status: "loading" };
case "SET_ERROR": return { ...state, status: "error", error: action.payload };
default: return state; // 未匹配的动作返回原 state
}
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
{state.status === "loading" && <Spinner />}
{state.status === "error" && <Alert>{state.error}</Alert>}
{state.items.map(item => (
<Item key={item.id} {...item} onRemove={() => dispatch({ type: "REMOVE", payload: item.id })} />
))}
</div>
);
}
[!tip] useReducer 的优势
- 动作语义化:
dispatch({ type: "ADD" })比setState()更能表达意图- 逻辑集中:所有变更规则集中在 reducer 函数里,方便测试
- 批量关联更新:一个 action 可以同时更新多个相互关联的子值
- DevTools 友好:配合 Redux DevTools 可实现时间旅行调试
useState vs useReducer 决策表
| 维度 | 选 useState | 选 useReducer |
|---|---|---|
| state 类型 | 简单标量(number, boolean, string) | object / array,结构较复杂 |
| 下一个 state | 不依赖旧值,或直接给出 | 依赖旧值,且有分支逻辑 |
| 变更来源 | 单一触发点 | 多个可能的 action 类型 |
| 子值联动 | 无需联动 | 一次操作需更新多个子值 |
| 可测试性 | 不需要单独测试 | reducer 是纯函数,可独立测试 |
[!example] 同一个场景的两种写法对比
假设实现一个计数器,支持 increment/decrement/reset:
// useState 版本 const [n, setN] = useState(0); // reset 需要知道初始值,不够自包含 // useReducer 版本 function reducer(state, action) { switch (action.type) { case "INCREMENT": return { n: state.n + 1 }; case "DECREMENT": return { n: state.n - 1 }; case "RESET": return { n: INITIAL_VALUE }; // self-contained } }
什么时候不应该用 State?
[!important] 误区:把所有数据都塞进 state
不是所有数据都需要 state。以下情况应优先考虑替代方案:
// ❌ 不该存为 state:可以从 props 或其他 state 计算得出
const [fullName, setFullName] = useState("");
useEffect(() => setFullName(`${firstName} ${lastName}`), [firstName, lastName]);
// ✅ 直接在渲染中计算
const fullName = `${firstName} ${lastName}`;
import { useRef } from "react";
// ❌ 不该存为 state:需要跨渲染保持可变引用(定时器 ID、DOM 节点、WebSocket)
let timerId = useRef<NodeJS.Timeout>(); // 不能在渲染期间赋值
// ✅ 用 useRef 存储非渲染相关的可变值
const timerRef = useRef<NodeJS.Timeout>();
timerRef.current = setTimeout(() => {}, 1000);
// ⚠️ 注意:修改 ref.current 不会触发重渲染!
import { useMemo, useCallback } from "react";
// ❌ 过度优化:每个小值都用 useMemo/useCallback
const [n, setN] = useState(0);
const doubled = useMemo(() => n * 2, [n]); // 多此一举
// ✅ useMemo/useCallback 只在昂贵计算或传给 memo 子组件时使用
性能注意事项
[!summary] 本节要点:state 的每一次 setter 调用都会触发当前组件重渲染,优化的核心思路是减少不必要的渲染和降低渲染成本。
-
拆分 state — 不相关的状态不要放在同一个 state 变量里,避免一处变更导致整个大对象所在组件全部重渲染
-
惰性初始化 —
useState(() => computeExpensiveValue())中的工厂函数只在首次挂载时执行,适合初始化开销大的场景 -
稳定引用 — 组件函数本身在每次渲染都是新引用,如需传给子组件,优先用
useCallback;跨渲染保存可变引用用useRef
// 惰性初始化
const [cache, setCache] = useState<Map<string, any>>(() => new Map());
// useRef 存储 callback(配合 memo 使用)
const callbackRef = useRef(callback);
callbackRef.current = callback;
// 子组件中通过 callbackRef.current 访问
[!tip] React 18 批处理升级
React 18 起,所有状态更新都自动批处理,包括:
- Promise 回调内的 setState
- setTimeout 内的 setState
- 原生事件处理器内的 setState
不再需要
unstable_batchedUpdates,也不再需要担心手动批处理的问题。
关联笔记
- hhs/REACT/1. 基础篇/03-组件与 Props
- hhs/REACT/2. Hooks 篇/05-核心 Hooks
- hhs/REACT/2. Hooks 篇/06-性能优化 Hooks
- hhs/REACT/4. 进阶篇/11-组件通信模式
- hhs/REACT/5. 工程实践篇/15-性能优化