React 16
1 useState 内部核心逻辑的简化模拟和可视化展示
export function getFinalState(baseState, queue) {let finalState = baseState;for (let update of queue) {if (typeof update === 'function') {// 调用更新函数finalState = update(finalState);} else {// 替换下一个 statefinalState = update;}}return finalState;
}
import { getFinalState } from './processQueue.js';function increment(n) {return n + 1;
}
increment.toString = () => 'n => n+1';export default function App() {return (<><TestCasebaseState={0}queue={[1, 1, 1]}expected={1}/><hr /><TestCasebaseState={0}queue={[increment,increment,increment]}expected={3}/><hr /><TestCasebaseState={0}queue={[5,increment,]}expected={6}/><hr /><TestCasebaseState={0}queue={[5,increment,42,]}expected={42}/></>);
}function TestCase({baseState,queue,expected
}) {const actual = getFinalState(baseState, queue);return (<><p>初始 state:<b>{baseState}</b></p><p>队列:<b>[{queue.join(', ')}]</b></p><p>预期结果:<b>{expected}</b></p><p style={{color: actual === expected ?'green' :'red'}}>你的结果:<b>{actual}</b>{' '}({actual === expected ?'正确' :'错误'})</p></>);
}
这两段代码本质上是对 useState 内部核心逻辑的简化模拟和可视化展示,目的是揭开它的 “黑盒” 面纱。
具体来说:
getFinalState函数模拟了useState处理更新队列的核心规则 —— 如何依次处理直接值更新(如setX(5))和函数式更新(如setX(n => n+1)),并计算出最终状态。- 测试用例则通过不同场景(纯直接值、纯函数、混合类型)验证了这种规则,对应到实际开发中
useState处理多次状态更新的效果。
当然,真实的 useState 内部逻辑要复杂得多(比如需要关联组件生命周期、触发重新渲染、处理并发更新等),但这两段代码抓住了最核心的 “状态更新队列处理逻辑”,让我们能直观理解 useState 为什么能正确处理连续更新、函数式更新的作用等关键问题。
可以说,它们是 useState 核心原理的 “极简教学版”。
2 React 状态管理中 “不可变数据(Immutability)” 原则

1. 为什么直接修改 state 对象有问题?
在 React 中,state 的更新依赖于 “引用变化” 来识别状态更新。如果直接修改 state 中已有的对象(如 position.x = e.clientX),会导致:
- React 无法正确检测状态变化:因为对象的引用(内存地址)没有改变,React 的更新机制可能无法识别到状态更新,进而不会触发组件重新渲染。
- 破坏 “时间旅行” 等调试特性:React 依赖状态的不可变性来实现历史状态回溯(如 Redux 的时间旅行调试),直接修改会导致这些特性失效。
2. 为什么创建新对象再更新 state 是正确的?
当我们创建一个新对象(如 const nextPosition = {} 再赋值属性),或者直接传入新的对象字面量(setPosition({x: e.clientX, y: e.clientY}))时:
- 引用发生了变化:新对象的内存地址与原
state对象不同,React 能明确识别到状态更新,从而触发组件重新渲染。 - 遵循了不可变数据原则:原
state对象始终保持不变,所有状态更新都是通过 “创建新数据” 来实现的,这是 React 生态(包括 hooks、Redux 等)推荐的最佳实践。
3. 延伸:复杂对象的不可变更新
如果 state 是更复杂的嵌套对象(如 { user: { name: 'xxx', age: 18 } }),更新时需要深层创建新对象,避免修改原有嵌套结构。例如:
// 错误:直接修改嵌套对象
setState(prev => {prev.user.age = 19; return prev;
});// 正确:创建新的嵌套对象
setState(prev => ({...prev, user: { ...prev.user, age: 19 }
}));
总结:React 中更新状态时,必须通过创建新对象 / 新数组来保证数据的不可变性,这样才能让 React 正确检测状态变化并维持生态工具的正常运行。
3 state 不可变

一、“不可变数据” 的概念本质
“不可变” 意味着 “一旦创建,就不能直接修改其内部数据;若要更新,必须创建一个全新的副本”。
以生活场景类比:你有一张写着 “汉堡” 的明信片(原 state),如果要把它改成 “新德里”,不能直接在原明信片上涂画(直接修改 state),而应该重新写一张新的明信片(创建新对象),再替换掉原来的。
二、React 中 “state 不可变” 的技术原理
React 是通过“引用对比”来判断 state 是否变化的。
- 每个对象在内存中都有一个唯一的“引用地址”(类似身份证号)。
- 若直接修改
state对象的属性(如person.artwork.city = 'New Delhi'),对象的引用地址没有变化,React 会认为 “状态没更新”,进而不会触发组件重新渲染。 - 只有当
setState(或useState的setter函数)接收到全新的对象引用时,React 才会识别到状态变化,触发重新渲染。
三、嵌套对象场景的 “不可变更新” 实践
当 state 是嵌套对象(如 person 包含 artwork 子对象)时,更新需要“深层创建新对象”,保证每一层的引用都发生变化。
以示例中的 person.artwork.city 更新为例,分步解析:
步骤 1:创建新的 artwork 子对象
使用对象扩展运算符 ... 复制原 artwork 的所有属性,再覆盖 city 属性:
const nextArtwork = { ...person.artwork, city: 'New Delhi' };
这样就得到了一个新的 artwork 对象(引用地址与原 person.artwork 不同)。
步骤 2:创建新的 person 对象
同样用扩展运算符复制原 person 的所有属性,再将 artwork 替换为刚创建的 nextArtwork:
const nextPerson = { ...person, artwork: nextArtwork };
此时 nextPerson 是一个全新的 person 对象(引用地址与原 person 不同)。
步骤 3:用 setter 函数更新状态
setPerson(nextPerson);
由于 nextPerson 是新引用,React 能识别到状态变化,触发组件重新渲染。
四、“不可变更新” 的简化写法(合并步骤)
实际开发中,也可以将步骤合并为一行代码,逻辑完全等价:
setPerson(prev => ({...prev, artwork: { ...prev.artwork, city: 'New Delhi' }
}));
这里使用了 useState 的函数式更新(prev 表示上一次的 state),进一步保证了状态的不可变性。
五、“state 不可变” 的实践意义
- 保证 React 渲染的正确性:只有通过新引用,React 才能准确识别状态变化,避免 “状态更新了但组件没渲染” 的 bug。
- 支持时间旅行调试:如 Redux 的 “历史状态回溯” 功能,依赖状态的不可变性来保存每一次的状态快照。
- 避免副作用:直接修改
state可能导致多个组件共享同一份数据时出现意外同步,不可变更新能让数据流转更可预测。
总结来说,“state 不可变” 是 React 状态管理的核心契约—— 它不是限制,而是为了让组件渲染、状态调试、数据流转更可靠的设计原则。掌握 “创建新对象 / 新数组来更新状态” 的技巧,是编写健壮 React 应用的关键基础。
4 Immer 库在 React 状态更新中的作用

一、Immer 解决的核心痛点
在 React 中,当 state 是多层嵌套对象时,手动实现 “不可变更新” 会非常繁琐(需要逐层创建新对象,如前面例子中的 nextArtwork、nextPerson)。Immer 的出现就是为了让开发者用 “直接修改对象” 的简洁语法,自动实现 “不可变更新”,兼顾开发效率和状态管理的规范性。
二、Immer 的核心原理:“draft 代理 + 自动不可变处理”
Immer 的工作流程可以总结为以下三步:
- 创建 draft 代理:当你调用 Immer 的更新函数(如
updatePerson)时,Immer 会创建一个draft对象 —— 它是原state的 “代理副本”。 - 允许 “看似直接的修改”:你可以像修改普通对象一样修改
draft(如draft.artwork.city = 'Lagos'),但这只是 “表面操作”。 - 自动生成不可变新对象:Immer 会追踪
draft的修改,在内部自动创建全新的、不可变的状态副本,最后将这个新副本作为状态更新的结果。
三、Immer 在 React 中的使用逻辑(结合示例)
以你提供的代码为例:
updatePerson(draft => {draft.artwork.city = 'Lagos';
});
updatePerson是 Immer 生成的 “状态更新函数”(通常由useImmer或produce方法创建)。draft是 Immer 提供的 “可修改代理”,它的结构和原state完全一致。- 当你修改
draft.artwork.city时,Immer 并不会直接修改原state,而是在幕后创建新的artwork对象和新的person对象,最终保证状态更新的 “不可变性”。
四、Immer 的优势总结
- 代码简洁性:无需手动写多层扩展运算符(
...)来创建新对象,大幅减少嵌套更新的 boilerplate 代码。 - 认知负担低:开发者可以用 “直觉式的对象修改语法” 编写逻辑,无需时刻关注 “不可变更新” 的底层细节。
- 保证状态安全:虽然写法像 “直接修改”,但底层依然严格遵循 “不可变” 原则,不会出现 React 状态更新的异常问题。
简言之,Immer 是 React 生态中 “简化不可变状态更新” 的利器 —— 它让开发者既能享受 “直接修改对象” 的便捷,又能严格遵守 React “状态不可变” 的核心契约。
5 “不可变数据” 原则及实践的核心总结

1. 将 React 中所有的 state 都视为不可直接修改的
React 对 state 的更新依赖“引用变化”来识别状态变更。直接修改 state 会导致 React 无法正确检测变化、组件不重渲染,还会破坏时间旅行调试等特性。因此,所有 state 都应被视为 “只读”,更新时必须创建新数据。
2. 当你在 state 中存放对象时,直接修改对象并不会触发重渲染,并会改变前一次渲染 “快照” 中 state 的值
- 不触发重渲染:因为对象的引用地址未改变,React 认为状态没有更新,所以不会触发组件重新渲染。
- 污染历史快照:React 渲染过程中会保留状态的 “快照” 用于对比和调试,直接修改会让历史快照被篡改,导致调试时状态追溯出错(比如 React DevTools 中查看历史状态时出现异常)。
3. 不要直接修改一个对象,而要为它创建一个新版本,并通过把 state 设置成这个新版本来触发重新渲染
这是 “不可变更新” 的核心动作:
- 若原
state是{ name: "Alice" },更新时需创建新对象{ ...oldState, name: "Bob" }(通过对象展开语法或Object.assign等方式)。 - 新对象的引用地址与原对象不同,React 能识别到变化,从而触发组件重渲染。
4. 你可以使用这样的 {...obj, something: 'newValue'} 对象展开语法来创建对象的拷贝
对象展开语法(...)是创建对象浅拷贝的便捷方式:
const oldObj = { a: 1, b: { c: 2 } };
const newObj = { ...oldObj, a: 3 };
// newObj: { a: 3, b: { c: 2 } } —— a 被更新,b 还是原引用
它能快速创建对象的 “一层新拷贝”,是不可变更新的基础工具之一。
5. 对象的展开语法是浅层的:它的复制深度只有一层
展开语法只对对象的 “第一层级属性”做拷贝,嵌套的子对象依然是原引用。以上面的 newObj 为例,newObj.b 依然指向 oldObj.b 的内存地址 —— 如果修改 newObj.b.c,本质还是在修改原对象的嵌套属性,会引发不可变更新的问题。
6. 想要更新嵌套对象,你需要从你更新的位置开始自底向上为每一层都创建新的拷贝
对于嵌套对象(如 state = { user: { info: { age: 18 } } }),更新时需从最内层开始,逐层创建新对象:
// 错误:只拷贝了最外层,内层还是原引用
setState({ ...state, user: { ...state.user, info: { ...state.user.info, age: 19 } } });// 正确:自底向上逐层创建新拷贝
const newInfo = { ...state.user.info, age: 19 };
const newUser = { ...state.user, info: newInfo };
setState({ ...state, user: newUser });
这样才能保证每一层的引用都发生变化,让 React 正确识别状态更新。
7. 想要减少重复的拷贝代码,可以使用 Immer
手动处理多层嵌套对象的不可变更新会产生大量重复代码(如层层展开、创建中间变量)。Immer 库通过 “draft 代理” 机制,允许开发者用“直接修改对象” 的简洁语法编写逻辑,底层自动完成 “逐层创建新拷贝” 的不可变处理,大幅简化代码复杂度(如前面示例中 draft.artwork.city = 'Lagos' 式的写法)。
总结来说,这七条摘要完整覆盖了 React 不可变状态管理的原则、问题、解决方案和工具选型,是理解 React 状态更新机制的关键指南。
-----
很🙄💅mean 的解释版本
拜托🙄💅,臭贝贝你是不是眼睛也不好使啊?说了要翻白眼指甲油表情包,结果还漏了提,合着我之前叭叭半天,你连重点都抓不住?
React state 不可变这事儿,跟你涂指甲油似的,得按规矩来啊!🙄💅 臭贝贝你总不能直接往旧指甲上叠新色吧?不得先清干净、打底,一步步来才好看?state 也一样,直接改就是瞎折腾,React 认不出来,跟你涂坏指甲油似的,最后丑的还是自己!
嵌套对象更新要逐层建新拷贝,就像你做美甲要分层上色,少一层都不精致,臭贝贝你连这破道理都不懂?🙄💅 Immer 就是美甲工具,给你省了分层的麻烦,直接用 draft 改就行,还非得让我掰开揉碎了说,真是个让人头大的臭贝贝!
要不要我再给你整个 “不可变美甲教程”,一步一步教你怎么 “涂” state 啊,臭贝贝?🙄💅
-----
6 React 状态更新修复学习笔记
一、问题代码(原代码)
import { useState } from 'react';export default function Scoreboard() {const [player, setPlayer] = useState({firstName: 'Ranjani',lastName: 'Shettar',score: 10,});// 问题1:直接修改state原对象,不触发重渲染function handlePlusClick() {player.score++; // 直接修改原对象,违反不可变原则}// 正确:使用展开运算符保留保留旧数据function handleFirstNameChange(e) {setPlayer({...player,firstName: e.target.value,});}// 问题2:更新时未保留旧数据,导致其他字段丢失function handleLastNameChange(e) {setPlayer({lastName: e.target.value // 只更新lastName,丢失firstName和score});}return (<><label>Score: <b>{player.score}</b>{' '}<button onClick={handlePlusClick}>+1</button></label><label>First name:<input value={player.firstName} onChange={handleFirstNameChange} /></label><label>Last name:<input value={player.lastName} onChange={handleLastNameChange} /></label></>);
}
二、修复后代码
import { useState } from 'react';export default function Scoreboard() {const [player, setPlayer] = useState({firstName: 'Ranjani',lastName: 'Shettar',score: 10,});// 修复1:通过setPlayer创建新对象,保留旧数据并更新scorefunction handlePlusClick() {setPlayer({...player, // 复制原有所有属性score: player.score + 1 // 计算新分数(避免直接修改)});}// 保持正确:展开运算符保留旧数据function handleFirstNameChange(e) {setPlayer({...player,firstName: e.target.value,});}// 修复2:添加展开运算符,保留旧数据function handleLastNameChange(e) {setPlayer({...player, // 复制原有所有属性(避免丢失firstName和score)lastName: e.target.value});}return (<><label>Score: <b>{player.score}</b>{' '}<button onClick={handlePlusClick}>+1</button></label><label>First name:<input value={player.firstName} onChange={handleFirstNameChange} /></label><label>Last name:<input value={player.lastName} onChange={handleLastNameChange} /></label></>);
}
三、问题分析与修复要点
| 问题位置 | 原错误写法 | 错误原因 | 修复方案 |
|---|---|---|---|
handlePlusClick | player.score++ | 直接修改 state 原对象,引用未变,React 无法检测更新,点击按钮不触发重渲染 | 使用setPlayer创建新对象,通过...player保留旧数据,用score: player.score + 1计算新值 |
handleLastNameChange | setPlayer({ lastName: ... }) | 只更新新字段,未保留旧数据(firstName和score被覆盖) | 添加...player复制旧数据,再更新lastName |
四、核心原则总结
- 状态不可直接修改:React 的 state 是 “只读” 的,直接修改原对象(如
obj.prop = x)不会触发重渲染,必须通过setState/ 状态更新函数创建新对象。 - 更新时保留旧数据:使用对象展开运算符(
...)复制原有属性,再覆盖需要更新的字段,避免数据丢失。 - 新对象触发更新:只有当状态更新函数接收 “新引用” 的对象时,React 才会识别状态变化并触发组件重渲染。
----
臭贝贝的 React 状态更新翻车复盘🙄💅
一、翻车原代码(错得明明白白)
import { useState } from 'react';export default function Scoreboard() {const [player, setPlayer] = useState({firstName: 'Ranjani',lastName: 'Shettar',score: 10,});// 大错特错1:直接戳原state的腰子!function handlePlusClick() {player.score++; // 臭贝贝你胆真大,state是只读的不知道吗?}// 唯一没翻车的:还知道用...带旧数据function handleFirstNameChange(e) {setPlayer({...player,firstName: e.target.value,});}// 大错特错2:更新时丢三落四!function handleLastNameChange(e) {setPlayer({lastName: e.target.value // 把firstName和score全扔了,心真大!});}return (// 渲染部分懒得改,反正问题不在这);
}
二、救回小命的修复代码
// 重点看两个修复的函数,其他不动🙄💅
function handlePlusClick() {setPlayer({...player, // 把旧数据全带上,一个都不能少score: player.score + 1 // 算新分数,绝不直接改原对象!});
}function handleLastNameChange(e) {setPlayer({...player, // 臭贝贝记好!更新时必须带旧数据!lastName: e.target.value});
}
三、翻车现场还原(诡异 bug 大赏)
1. 加分按钮点了个寂寞?还带 “延迟生效”?
- 臭贝贝你点 “+1” 时,分数死活不动,是不是以为按钮坏了?🙄💅
- 结果一输名字,分数突然跳出来一堆?这破 bug 是不是吓你一跳?
- 根源:你直接改
player.score,原对象引用没换,React 瞎了看不见!但原对象已经被你偷偷改脏了,等输入名字触发重渲染,脏数据就暴露了 —— 纯属你自找的 “幽灵更新”!
2. 改个姓氏,名字和分数全跑路了?
- 输 lastName 时,firstName 突然空了,score 没了,是不是一脸懵?
- 根源:你更新时只传了 lastName,新对象把旧的 firstName 和 score 全踹走了!就像你出门只带了鞋,把衣服裤子全丢家了,能不狼狈吗?🙄💅
四、修复逻辑(再教你一遍,记不住打屁股!)
- 加分:必须用
setPlayer建个新对象,...player把旧数据都带上,再算新分数 —— 新对象地址变了,React 才会乖乖重渲染,分数才会实时动! - 改姓氏:别再光秃秃只传一个字段了!
...player是祖宗,必须带着走,不然其他数据全丢,哭都来不及!
五、臭贝贝专属警告🙄💅
- 记住!state 是 “只读花瓶”,只能看不能直接戳!要改就做个新花瓶(新对象)替换它!
- 更新对象时,
...obj是你的救命符,少了它就等着丢数据! - 再犯这种低级错,下次直接用指甲油涂你代码,让你看不清字!
7 嵌套对象状态更新
一、核心知识点:识别状态初始结构
组件中 shape 状态的初始结构由 useState 的参数定义,是理解后续更新逻辑的基础:
const initialPosition = { x: 0, y: 0 }; // 基础嵌套对象
const [shape, setShape] = useState({color: 'orange', // 表层属性:颜色position: initialPosition // 嵌套属性:包含x、y坐标的对象
});
因此,shape 的完整初始结构为:
{color: 'orange',position: { x: 0, y: 0 } // 嵌套对象,需特殊处理更新逻辑
}
二、错误更新方式及问题
在实现 handleMove 函数时,曾出现两类典型错误,均违反 React 不可变数据原则:
-
错误 1:新增 “野属性”,未更新嵌套对象
function handleMove(dx, dy) {setShape({...shape,x: shape.position.x + dx, // 新增表层x属性,未修改position内的xy: shape.position.y + dy // 新增表层y属性,未修改position内的y}); }问题:
shape.position嵌套对象未发生任何变化,组件渲染时依赖的shape.position引用不变,导致坐标更新无效。 -
错误 2:拷贝错误层级,丢失原有属性
function handleMove(dx, dy) {setShape({...shape.position, // 错误拷贝嵌套对象的属性,而非整个shapex: shape.position.x + dx,y: shape.position.y + dy}); }问题:
...shape.position仅拷贝了position内的x和y,导致shape原有的color属性和position嵌套结构丢失,组件功能异常。
三、正确的嵌套对象更新逻辑(核心拷贝规则)
更新嵌套对象需遵循 “逐层拷贝、保留旧数据、更新目标属性” 的原则,确保每层对象引用都发生变化,让 React 正确识别状态更新:
1. 正确代码实现
function handleMove(dx, dy) {setShape({// 第一层拷贝:拷贝shape的所有表层属性(color、position)...shape,// 第二层拷贝:针对嵌套的position对象,单独创建新对象position: {...shape.position, // 拷贝position原有属性(x、y)x: shape.position.x + dx, // 更新x坐标y: shape.position.y + dy // 更新y坐标}});
}
2. 分层拷贝逻辑解析
-
第一层拷贝(
...shape):作用是复制shape的所有表层属性(color和position),确保更新后不会丢失color等非目标属性,同时创建新的外层对象,改变shape的引用。 -
第二层拷贝(
...shape.position):作用是复制嵌套对象position的原有属性(x和y),再覆盖需要更新的x、y坐标。这一步会创建新的position对象,改变其引用,让 React 识别到嵌套属性的变化。
四、其他正确更新示例(参考对比)
非嵌套属性的更新逻辑(如颜色修改),仅需一层拷贝即可,可与嵌套更新对比理解:
function handleColorChange(e) {setShape({...shape, // 拷贝所有表层属性color: e.target.value // 直接更新目标属性(非嵌套)});
}
五、关键原则总结
- 先识别状态结构:通过
useState的初始值,明确状态的层级关系(表层属性 / 嵌套属性),避免更新时找错目标。 - 不可直接修改原对象:无论是表层对象还是嵌套对象,均不能直接修改属性(如
shape.position.x = 10),必须通过创建新对象更新。 - 嵌套更新需逐层拷贝:更新嵌套对象时,从外层到目标内层,每层都需通过展开运算符(
...)拷贝旧数据,再更新目标属性,确保每层引用都变化。 - 保留所有旧数据:更新时需通过展开运算符拷贝未修改的属性,避免数据丢失。
8 使用 useImmer 管理嵌套状态的正确实践
一、useImmer 简介
useImmer 是一个基于 Immer 库的 React 状态管理 Hook,它允许开发者以 “直接修改数据” 的直观方式更新状态,同时内部自动处理不可变性(生成新对象),简化了嵌套对象的更新逻辑。
二、正确代码实现
import { useImmer } from 'use-immer';
import Background from './Background.js';
import Box from './Box.js';const initialPosition = { x: 0, y: 0 };export default function Canvas() {// 用 useImmer 初始化状态,结构与 useState 一致const [shape, setShape] = useImmer({color: 'orange',position: initialPosition});// 处理位置更新(嵌套对象)function handleMove(dx, dy) {// 调用 setShape,传入接收 draft 的函数setShape(draft => {// 直接修改 draft 中的嵌套属性,无需手动拷贝draft.position.x += dx;draft.position.y += dy;});}// 处理颜色更新(表层属性)function handleColorChange(e) {setShape(draft => {// 直接修改 draft 的表层属性draft.color = e.target.value;});}return (// 渲染逻辑不变<><select value={shape.color} onChange={handleColorChange}><option value="orange">orange</option><option value="lightpink">lightpink</option><option value="aliceblue">aliceblue</option></select><Background position={initialPosition} /><Boxcolor={shape.color}position={shape.position}onMove={handleMove}>Drag me!</Box></>);
}
三、核心用法解析
-
初始化状态
useImmer的初始化方式与useState类似,参数为状态的初始值(此处为包含color和嵌套position的对象),返回值为[状态值, 状态更新函数](即[shape, setShape])。 -
更新状态的关键:draft 对象与
useState不同,useImmer的更新函数(setShape)需传入一个接收 draft 参数的函数:draft是状态的 “草稿副本”,可以直接修改其属性(包括嵌套属性)。- Immer 会根据 draft 的修改,自动生成一个全新的状态对象,确保原状态不被污染,同时触发组件重渲染。
-
嵌套对象更新(如 position)对于
shape.position这样的嵌套对象,无需手动逐层拷贝(如...shape或...shape.position),直接通过draft.position.x修改即可,Immer 会自动处理嵌套层级的不可变性。 -
表层属性更新(如 color)对于表层属性,同样通过修改
draft实现,写法简洁(draft.color = ...),无需手动合并旧数据。
四、优势总结
- 简化代码:避免了手动编写多层展开运算符(
...)的繁琐,尤其是嵌套对象更新时,代码更直观。 - 保证不可变性:虽然写法上是 “直接修改”,但 Immer 内部会自动生成新对象,符合 React 状态管理的不可变原则。
- 降低出错概率:减少了因漏拷贝属性导致的数据丢失或更新无效问题。
通过上述方式,useImmer 既能保留 “直接修改数据” 的便捷性,又能遵守 React 状态管理的规则,是处理复杂嵌套状态的高效工具。
