对象状态与嵌套更新
对象状态与嵌套更新 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
状态一变成嵌套对象,不可变更新就麻烦起来:一层的展开好写,两层以上的「层层新建」容易写漏。这一课用「章节 + 课时」的编辑数据带你把嵌套更新写扎实,并说清浅复制和直接修改各自会闯什么祸。
问题场景
课程编辑器里的数据长这样:
const [course, setCourse] = useState({
title: "React 入门",
sections: [
{ id: "s1", title: "章节一", lessons: [{ id: "l1", title: "课时 1", done: false }] },
],
});
需求是点一下课时切换完成状态。第一版图省事直接改了:
function toggleLesson(lessonId) {
const lesson = course.sections[0].lessons.find((l) => l.id === lessonId);
lesson.done = !lesson.done; // 直接改了状态里的对象
setCourse(course); // 再把同一个对象原样传回去
}
界面纹丝不动。setCourse(course) 传进去的还是同一个引用,React 一比:「新旧状态相等,没变化」,根本不渲染。改了内容但没换引用,React 就当什么都没发生。
核心解释
不可变更新的核心规则:要更新谁,就新建谁;它的每一层祖先也要新建。展开运算符 { ...obj } 只复制第一层,嵌套的对象和数组复制的仍然是引用——这就是浅复制,写嵌套更新时必须一层层展开:
function toggleLesson(course, lessonId) {
return {
...course,
sections: course.sections.map((section) => ({
...section,
lessons: section.lessons.map((lesson) =>
lesson.id === lessonId ? { ...lesson, done: !lesson.done } : lesson
),
})),
};
}
逐层读:外层 { ...course } 新建课程对象;sections.map 新建数组,每个 section 都展开新建;lessons.map 再往下走一层,只有命中的课时新建 { ...lesson, done: !lesson.done },其余课时对象保持原引用。这样 React 在任何一层比较都能看到「新引用」,更新链条完整。
「只有命中的才新建」是刻意的:没变的对象保留原引用,能让 React(以及将来的 memo 比较)快速跳过它们,这也是不可变数据换来的红利。
层级深、更新点多的表单类数据,官方建议考虑 useImmer 这类库让代码写成「直接改」的直觉形式——但库之前先把原生写法练熟,本课的实验只用展开运算符。
示例拆解
实验区是同一个编辑数据的两种更新写法,关键点如下:
- 数据两章、每章两课时,界面列出全部课时和完成状态;
- 原地改版:
lesson.done = !lesson.done后setCourse(course)——点什么界面都不动,数据其实已经变了(再触发一次别的渲染就会「跳变」,最迷惑的形态); - 展开版:上面的
toggleLesson完整跑通,每次点击界面立即翻转; - 每个课时只新建命中的那个对象,面板显示「未变课时引用保持原样」,对应不可变更新的性价比;
- 「改标题」按钮演示另一条常用路径:只展开一层
setCourse({ ...course, title })。
验证方式:原地改版点完课时再点「改标题」,完成状态突然集体跳出来——说明数据早被改了,只是 React 一直没渲染它。
动手任务
打开本课实验区,完成三个小改动:
- 加「新增课时」按钮:往指定章节的
lessons数组里追加一条,注意数组和新 section 都要新建。 - 给 section 加「重命名」功能,练习在
sections.map里只新建命中的 section。 - 把展开版的
lessons.map里漏写{ ...lesson }展开(直接返回改后的旧对象)观察界面变化,理解每一层为什么要新建。
常见错误
- 直接修改嵌套对象再 setState 同一引用:React 认为状态没变,不渲染;更新时命中对象必须新建。
- 以为
{ ...obj }深拷贝了:浅复制只到第一层,嵌套层还是共享引用;更新哪层就在哪层展开。 - 展开时把没改的层也全量新建:功能对,但每次更新所有对象身份都变,破坏了「未变引用保持原样」的优化空间;只新建命中路径。
- 在 state 里存方法或 class 实例:展开复制会丢方法绑定,复杂数据优先存纯数据结构,行为放组件里。
检查与复盘
完成下方检查题确认理解。自查清单:
- 能说清「直接修改 + set 同一引用」为什么不触发渲染
- 三层嵌套的定向更新能一次写对:每层 map + 展开,命中才新建
- 明白浅复制与深复制的区别,知道
JSON.parse(JSON.stringify())为什么不该用来拷贝状态
参考资料
- React 官方文档:更新嵌套对象 —— 浅复制与层层新建的官方教程
- React 官方文档:更新数组中的对象 —— 数组与对象混合结构的更新清单
读完这一课了?
标记完成后,工作台会安排接下来的学习。