useImmer 与复杂状态更新
使用 useImmer 通过 Draft 更新嵌套对象和数组,同时保持 React state 的不可变性。
核心结论
useImmer 允许业务代码修改临时 Draft,Immer 再生成不可变的下一份 state,并复用没有变化的分支。它改善的是复杂本地状态的更新表达,不会改变 React 的 state snapshot 模型。
Draft 到新状态
组件关系与数据流
State SnapshotUI / RenderProducerProxy Draft
更新焦点写法看起来像可变更新,但 React state 没有被直接修改;只有变化路径会创建新引用。
实线表示状态生产过程,虚线表示用户事件和未变化分支的引用复用。
flowchart TB
accTitle: useImmer 从 Draft 修改到 React 渲染的数据流
accDescr: 用户事件调用 updatePlan 并传入 producer。Immer 基于当前 state 创建 Proxy Draft,记录嵌套写操作,生成保持不可变性的下一个状态;发生变化的路径获得新引用,未变化分支复用旧引用,最后驱动 React 重新渲染。
event["用户编辑 / 勾选 / 删除"] -.-> producer{{"updatePlan(producer)"}}
current(["Current State"]) --> draft[("Proxy Draft")]
producer -->|"修改嵌套字段"| draft
draft --> changes{{"Immer 记录变化路径"}}
changes --> next(["Next State"])
current -.->|"复用未变化分支"| next
next --> render["React 重新渲染"]
class current,next state
class event,render component
class producer,changes process
class draft resource
classDef state fill:#fffdf8,stroke:#149eca,color:#25221d,stroke-width:1.2px
classDef component fill:#e4f5fa,stroke:#149eca,color:#25221d,stroke-width:2px,font-weight:600
classDef process fill:#f4fafb,stroke:#149eca,color:#25221d,stroke-width:1.6px
classDef resource fill:#eee9df,stroke:#817a70,color:#25221d,stroke-width:1.4px业务代码修改的是临时 Draft,Immer 负责生成不可变的新状态并执行结构共享。
完整 Demo 源码
以下代码直接读取在线 Demo 使用的源文件,没有省略或改写。
React 源码
import { useRef, useState } from "react";import type { ChangeEvent, SubmitEvent } from "react";import { useImmer } from "use-immer";import { createInitialPlan } from "./initialPlan";import type { PlanLevel, StudyPlan } from "./initialPlan";import "./ImmerDemo.css";export function ImmerDemo() { const [plan, updatePlan] = useImmer<StudyPlan>(createInitialPlan); const [newTaskTitle, setNewTaskTitle] = useState(""); const [targetModuleId, setTargetModuleId] = useState("react"); const nextTaskId = useRef(1); const tasks = plan.modules.flatMap((module) => module.tasks); const completedCount = tasks.reduce( (count, task) => count + Number(task.completed), 0, ); const progress = tasks.length ? Math.round((completedCount / tasks.length) * 100) : 0; function changePlanTitle(event: ChangeEvent<HTMLInputElement>) { updatePlan((draft) => { draft.title = event.target.value; }); } function changeOwnerName(event: ChangeEvent<HTMLInputElement>) { updatePlan((draft) => { draft.owner.name = event.target.value; }); } function changeLevel(event: ChangeEvent<HTMLSelectElement>) { updatePlan((draft) => { draft.owner.level = event.target.value as PlanLevel; }); } function toggleTask(moduleId: string, taskId: string) { updatePlan((draft) => { const module = draft.modules.find((item) => item.id === moduleId); const task = module?.tasks.find((item) => item.id === taskId); if (task) task.completed = !task.completed; }); } function renameTask(moduleId: string, taskId: string, title: string) { updatePlan((draft) => { const module = draft.modules.find((item) => item.id === moduleId); const task = module?.tasks.find((item) => item.id === taskId); if (task) task.title = title; }); } function removeTask(moduleId: string, taskId: string) { updatePlan((draft) => { const module = draft.modules.find((item) => item.id === moduleId); if (!module) return; module.tasks = module.tasks.filter((task) => task.id !== taskId); }); } function completeModule(moduleId: string) { updatePlan((draft) => { const module = draft.modules.find((item) => item.id === moduleId); if (!module) return; for (const task of module.tasks) task.completed = true; }); } function addTask(event: SubmitEvent<HTMLFormElement>) { event.preventDefault(); const title = newTaskTitle.trim(); if (!title) return; updatePlan((draft) => { const module = draft.modules.find((item) => item.id === targetModuleId); module?.tasks.push({ id: `custom-${nextTaskId.current}`, title, completed: false, }); }); nextTaskId.current += 1; setNewTaskTitle(""); } function resetPlan() { updatePlan(createInitialPlan()); setNewTaskTitle(""); setTargetModuleId("react"); nextTaskId.current = 1; } return ( <div className="immer-demo not-content"> <header className="immer-demo__header"> <div> <span className="demo-tag">useImmer state</span> <h2>{plan.title || "未命名学习计划"}</h2> <p> {plan.owner.name || "未填写姓名"} · {plan.owner.level} </p> </div> <button className="immer-demo__button" type="button" onClick={resetPlan}> 重置计划 </button> </header> <div className="immer-demo__editor"> <label> 计划名称 <input value={plan.title} onChange={changePlanTitle} /> </label> <label> 学习者 <input value={plan.owner.name} onChange={changeOwnerName} /> </label> <label> 当前阶段 <select value={plan.owner.level} onChange={changeLevel}> <option value="初级">初级</option> <option value="进阶">进阶</option> </select> </label> </div> <div className="immer-demo__progress"> <div> <span>整体进度</span> <strong> {completedCount} / {tasks.length} 项 · {progress}% </strong> </div> <div className="immer-demo__progress-track" role="progressbar" aria-label="学习计划完成进度" aria-valuemin={0} aria-valuemax={100} aria-valuenow={progress} > <span style={{ width: `${progress}%` }} /> </div> </div> <div className="immer-demo__modules"> {plan.modules.map((module) => ( <section className="immer-demo__module" key={module.id}> <header> <div> <h3>{module.title}</h3> <span> {module.tasks.filter((task) => task.completed).length} /{" "} {module.tasks.length} 完成 </span> </div> <button className="immer-demo__text-button" type="button" onClick={() => completeModule(module.id)} disabled={module.tasks.length === 0} > 全部完成 </button> </header> {module.tasks.length ? ( <ul> {module.tasks.map((task) => ( <li className={task.completed ? "is-completed" : undefined} key={task.id} > <input type="checkbox" checked={task.completed} aria-label={`${task.completed ? "取消完成" : "完成"} ${task.title}`} onChange={() => toggleTask(module.id, task.id)} /> <input className="immer-demo__task-title" value={task.title} aria-label={`编辑任务:${task.title}`} onChange={(event) => renameTask(module.id, task.id, event.target.value) } /> <button className="immer-demo__remove" type="button" aria-label={`删除任务:${task.title}`} onClick={() => removeTask(module.id, task.id)} > 删除 </button> </li> ))} </ul> ) : ( <p className="immer-demo__empty">该模块还没有任务。</p> )} </section> ))} </div> <form className="immer-demo__add" onSubmit={addTask}> <label> 新任务 <input value={newTaskTitle} placeholder="例如:完成一次状态重构" onChange={(event) => setNewTaskTitle(event.target.value)} /> </label> <label> 添加到 <select value={targetModuleId} onChange={(event) => setTargetModuleId(event.target.value)} > {plan.modules.map((module) => ( <option value={module.id} key={module.id}> {module.title} </option> ))} </select> </label> <button className="immer-demo__button immer-demo__button--primary" type="submit" disabled={!newTaskTitle.trim()} > 添加任务 </button> </form> </div> );}在线 Demo
修改计划、编辑任务、批量完成或增删任务,观察多层对象与数组如何保持易读的更新代码。
useImmer state
React × Vue 学习计划
小明 · 初级
整体进度2 / 5 项 · 40%
React 核心
1 / 3 完成Vue 核心
1 / 2 完成使用边界
- 只能修改 updater 收到的
draft,不要直接修改当前 React state。 - 一次 updater 中选择“修改 Draft”或“返回替代状态”,不要混用。
- 简单布尔值或单层对象通常不需要 Immer,普通
useState更直接。 - 业务操作逐渐形成明确事件时,可以进一步使用
useImmerReducer。 useImmer管理本地客户端状态;远程服务端状态应交给TanStack Query。