--- tags: [React, State, Immutability, Hooks, Frontend] create time: 2026-04-29 22:03 --- # State 与不可变性 ## 概述 State 是 React 组件的数据源。**不可变性**(Immutability)是 React 状态管理的基石——React 通过引用比较判断 state 是否变化,只有创建了新的引用才会触发重渲染。理解这一点,就能避免 "数据变了但 UI 没更新" 这一经典 Bug。 ## 为什么必须遵守不可变性? ```mermaid 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 基础用法 ```tsx const [count, setCount] = useState(0); const [user, setUser] = useState<{ name: string; age: number }>({ name: "", age: 0 }); const [items, setItems] = useState([]); // setState 可以接收值或 updater 函数 setCount(c => c + 1); // 推荐:基于旧值时用 updater,避免闭包陷阱 ``` ### Updater 函数模式 当新 state 依赖旧 state 时,必须使用 updater 形式: ```tsx // 场景:快速连续点击,用值可能导致丢失更新 handleClick = () => { setCount(count + 1); // ❌ 如果 count 被缓存,可能只+1 setCount(count + 1); }; handleClick = () => { setCount(c => c + 1); // ✅ 每次都拿到最新值,两次点击正确 +2 setCount(c => c + 1); }; ``` > [!tip] 最佳实践 > 养成习惯:**凡是新值依赖于旧值,一律用 updater 函数**。即使当前看起来没问题,也能预防未来重构引入的闭包问题。 ## 不可变更新模式 ### 数组操作 ```tsx 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] 常见陷阱 > > ```tsx > // ❌ sort()/reverse()/push()/splice() 都原地修改原数组! > list.sort(); // list 引用不变 > setList(list); // React 跳过渲染 > > // ✅ 先拷贝再操作 > setList([...list].sort()); > ``` ### 对象更新 ```tsx 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 请求变更。 ```mermaid 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 ``` ```tsx // 状态提升后 function App() { const [countA, setCountA] = useState(0); const [countB, setCountB] = useState(0); return ( <>

总和: {countA + countB}

); } ``` > [!info] 何时使用状态提升? > - 两个或多个组件需要同步同一份数据 > - 一个组件的交互需要影响其他组件的行为 > - 没有更好的替代方案(如 Context、状态管理库)之前,先从状态提升开始 ## useReducer:管理复杂 state 当 state 逻辑变复杂时,`useReducer` 比 `useState(updater)` 更清晰: ```tsx 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 (
{state.status === "loading" && } {state.status === "error" && {state.error}} {state.items.map(item => ( dispatch({ type: "REMOVE", payload: item.id })} /> ))}
); } ``` > [!tip] useReducer 的优势 > > 1. **动作语义化**:`dispatch({ type: "ADD" })` 比 `setState()` 更能表达意图 > 2. **逻辑集中**:所有变更规则集中在 reducer 函数里,方便测试 > 3. **批量关联更新**:一个 action 可以同时更新多个相互关联的子值 > 4. **DevTools 友好**:配合 Redux DevTools 可实现时间旅行调试 ### useState vs useReducer 决策表 | 维度 | 选 useState | 选 useReducer | |------|------------|--------------| | state 类型 | 简单标量(number, boolean, string) | object / array,结构较复杂 | | 下一个 state | 不依赖旧值,或直接给出 | 依赖旧值,且有分支逻辑 | | 变更来源 | 单一触发点 | 多个可能的 action 类型 | | 子值联动 | 无需联动 | 一次操作需更新多个子值 | | 可测试性 | 不需要单独测试 | reducer 是纯函数,可独立测试 | > [!example] 同一个场景的两种写法对比 > > 假设实现一个计数器,支持 increment/decrement/reset: > > ```tsx > // 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。以下情况应优先考虑替代方案: ```tsx // ❌ 不该存为 state:可以从 props 或其他 state 计算得出 const [fullName, setFullName] = useState(""); useEffect(() => setFullName(`${firstName} ${lastName}`), [firstName, lastName]); // ✅ 直接在渲染中计算 const fullName = `${firstName} ${lastName}`; ``` ```tsx import { useRef } from "react"; // ❌ 不该存为 state:需要跨渲染保持可变引用(定时器 ID、DOM 节点、WebSocket) let timerId = useRef(); // 不能在渲染期间赋值 // ✅ 用 useRef 存储非渲染相关的可变值 const timerRef = useRef(); timerRef.current = setTimeout(() => {}, 1000); // ⚠️ 注意:修改 ref.current 不会触发重渲染! ``` ```tsx import { useMemo, useCallback } from "react"; // ❌ 过度优化:每个小值都用 useMemo/useCallback const [n, setN] = useState(0); const doubled = useMemo(() => n * 2, [n]); // 多此一举 // ✅ useMemo/useCallback 只在昂贵计算或传给 memo 子组件时使用 ``` ## 性能注意事项 > [!summary] 本节要点:state 的每一次 setter 调用都会触发当前组件重渲染,优化的核心思路是减少不必要的渲染和降低渲染成本。 1. **拆分 state** — 不相关的状态不要放在同一个 state 变量里,避免一处变更导致整个大对象所在组件全部重渲染 2. **惰性初始化** — `useState(() => computeExpensiveValue())` 中的工厂函数只在首次挂载时执行,适合初始化开销大的场景 3. **稳定引用** — 组件函数本身在每次渲染都是新引用,如需传给子组件,优先用 `useCallback`;跨渲染保存可变引用用 `useRef` ```tsx // 惰性初始化 const [cache, setCache] = useState>(() => 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-性能优化]]