数组与对象:不可变更新三件套Arrays and objects: three ways to update without changing the original
增、删、改一个列表,在 React 里为什么必须「造新的」而不是「改旧的」。Adding to, deleting from and editing a list: why React needs a new array instead of a changed one.
这一页有什么On this page8
- 01 为什么不能直接改Why you cannot change the original directly
- 02 三件套:新增 / 删除 / 就地替换The three operations: add, delete, replace in place
- 03 map / filter / find:三个都返回什么map / filter / find: what each one returns
- 04 对象展开:改一个字段,其他原样Object spread: change one field, keep the rest
- 05 解构:从对象里一次取好几个值Destructuring: take several values out of an object at once
- 练习 · 动手做Practice
- 常见错误Common mistakes
- 迁移模式Transfer
- 熟练用展开语法新增、filter 删除、map 就地替换Use spread syntax to add, filter to delete, and map to replace an item in place
- 解释「不可变更新」是什么意思,以及为什么 React 需要它Explain what it means to update without changing the original, and why React needs it
- 会用解构从对象里取值、给组件 props 取值Use destructuring to read values out of an object and out of component props
- 看到一段列表操作,能判断它改的是原数组还是新数组Look at some list code and say whether it changes the original array or builds a new one
Q1 的三道题,本质就是这三个操作各一次:Add 用展开、Delete 用 filter、Edit 用 map。GraphQL 那边的 createOrder 也要用 map 给每个 item 补价格。学会这一节,两门考试的数据操作部分就都通了。The three parts of Q1 are one of each operation: Add uses spread, Delete uses filter, Edit uses map. On the GraphQL side, createOrder also uses map to add a price to every item. Learn this lesson and the data handling of both exams is covered.
react-notes-app/src/components/NoteManager/index.tsx三个操作的真实用法都在这里The real use of all three operations is here
react-notes-app/src/components/NoteManager/index.tsx为什么不能直接改Why you cannot change the original directly
React 判断「要不要重新渲染」的方法,是比较「新旧是不是同一个东西」。React decides whether to render again by checking whether the new value is the same object as the old one.
先看这两段的区别:
左边 push 是改动原数组。数组变长了, 但它还是同一个数组 —— 内存里同一个地址。 右边的展开语法造了一个全新的数组, 内容是「旧的所有元素 + 新元素」。
React 在 setState 之后会做一次判断: 新值和旧值是不是同一个对象? 如果是同一个,它就认为「没变化」,直接跳过重新渲染。push 之后新旧是同一个数组,React 看不出变化, 界面就不更新 —— 数据其实变了,屏幕上却没反应。这是新手最常见的 bug。
所以规矩是:永远造新的,不改旧的。这个做法叫不可变更新(immutable update)。
Start with the difference between these two:
On the left, push mutates the original array. The array got longer, but it is still the same array — the same address in memory. On the right, spread syntax builds a brand new array whose contents are “every old element, plus the new one”.
After setState, React runs one check: are the new value and the old value the same object? If they are, it decides nothing changed and skips the re-render. After push, old and new are the same array, React sees no change, and the UI does not update — the data really did change, and the screen just sits there. This is the most common beginner bug there is.
So the rule is: always build a new one, never edit the old one. The practice has a name: immutable update.
三件套:新增 / 删除 / 就地替换The three operations: add, delete, replace in place
Q1 的三道题就是这三行。The three parts of Q1 are these three lines.
这三段是从 NoteManager 里原样摘出来的, 也就是这道题的标准答案:
注意三处细节:
setNotes(prev => ...)—— 传的是个函数,拿到的prev是「此刻最新的」值。 比直接用外面的notes变量安全(后面 useState 那节细讲)。- 删除用
!==而不是===——filter保留的是「返回 true 的元素」, 所以条件要写「不是要删的那个」。 - 更新用
map而不是「先删再加」 ——map逐个走过每个元素,是目标就换成新的、不是就原样保留。 这样顺序不变。而「先 filter 掉再 push 新的」会把这条 挪到末尾 —— 题目明确要求「原位置更新」,那样就错了。
These three are lifted verbatim out of NoteManager — they are the model answer for this question:
Three details to notice:
setNotes(prev => ...)— you pass a function, and theprevit hands you is the value as of right now. Safer than reaching for the outernotesvariable (the useState lesson goes into this).- Delete uses
!==, not===—filterkeeps the elements whose callback returns true, so the condition has to read “not the one being deleted”. - Update uses
map, not “delete then append” —mapwalks every element, swapping in the new one where it matches and leaving the rest as they were. That keeps the order intact. Filtering the old one out and pushing the new one moves that row to the end — and the task explicitly asks for an update in place, so that would be wrong.
react-notes-app/src/components/NoteManager/index.tsxmap / filter / find:三个都返回什么map / filter / find: what each one returns
这三个方法长得像,但返回的东西完全不同。混淆它们会写出很难查的 bug:
| 方法 | 返回 | 长度 | 典型用途 |
|---|---|---|---|
map | 新数组 | 和原数组一样长 | 逐个变形:渲染成 JSX、替换某一项、给每项补字段 |
filter | 新数组 | 可能更短 | 筛掉不要的:删除、搜索 |
find | 单个元素或 undefined | — | 找一个:按 id 取某条数据 |
这三个在两个考试里都真实出现过。map 在NoteTable 里把 notes 渲染成行;filter 在OrderDataSource.getOrdersByUserId 里按 userId 筛订单;find 在 OrderDataSource.getOrder 里按 id 取一条。
These three look alike, but what they hand back is completely different. Mixing them up produces bugs that are painful to track down:
| Method | Returns | Length | Typical use |
|---|---|---|---|
map | a new array | the same length as the original | Reshape each item: render to JSX, replace one entry, add a field to every entry |
filter | a new array | possibly shorter | Drop what you do not want: delete, search |
find | one element or undefined | — | Find one: fetch a single record by id |
All three show up for real in both exams. map renders notes into rows inside NoteTable; filter picks orders by userId in OrderDataSource.getOrdersByUserId; find pulls one record by id in OrderDataSource.getOrder.
graphql-federation-practice/node-subgraph/src/dataSources/orderDataSource.js对象展开:改一个字段,其他原样Object spread: change one field, keep the rest
数组用 [...arr, x],对象用 {...obj, key: v}。 后面的键会覆盖前面的:
这个写法在 Federation 那道题里有实际用途:createOrder 收到的 item 只有productId 和 quantity, 需要补上 price 才能算总价。
Arrays use [...arr, x]; objects use {...obj, key: v}. Later keys overwrite earlier ones:
This has a real use in the Federation question: the item that createOrder receives only carries productId and quantity, and you have to add price before you can work out a total.
解构:从对象里一次取好几个值Destructuring: take several values out of an object at once
你会在两个考试的每一个组件和 resolver 里看到它:
{ onSubmit, noteToEdit } 写在函数参数位置, 意思是「传进来的那个对象里,把 onSubmit 和noteToEdit 这两个键取出来当局部变量」。 resolver 的第三个参数 context 也是这么拆的。
You will run into it in every component and every resolver across both exams:
{ onSubmit, noteToEdit } written in the parameter position means “out of the object handed to me, pull the onSubmit and noteToEdit keys out as local variables”. A resolver’s third parameter, context, gets taken apart the same way.
动手做Get your hands on it
填空只是过渡。真正掌握的标准,是在没有答案的时候从头写出来 —— 所以做完 L2 之后一定要往 L3、L4 走。Filling blanks is a stepping stone. The real bar is writing it from nothing, so once L2 is comfortable, push on to L3 and L4.
这是 NoteManager 里三个 handler 的真实代码, 挖掉了决定行为的关键词。想清楚每个操作要「保留多少条」再填。
This is the real code of the three handlers in NoteManager, with the words that decide the behaviour removed. Before you fill each one in, work out how many items that operation has to keep.
这一类 bug 最难查,因为它不报错。 先看现象,判断类型,再找病灶 —— 别跳步。
This kind of bug is the hardest to find, because it reports no error. Read the symptom, classify it, then locate it. Do not skip a step.
初学者常见的几种写法错误Mistakes beginners actually make
下面每一段都是「能编译、但结果不对」或者「一跑就炸」的真实写法。先自己看出问题在哪,再看解释。Every snippet below either compiles and gives the wrong answer, or blows up on the first run. Spot the problem yourself before reading the explanation.
filter保留的是回调返回 true 的元素,所以删除操作的条件必须是!==。This keeps only the item you wanted to remove, and drops all the others. filter keeps the elements whose callback returns true, so the condition for deleting has to be !==.换一道题也能用Works on other problems too
考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.
- React 靠「是不是同一个对象」判断变化,所以必须造新的、不改旧的。React looks at whether it is the same object to decide that something changed, so build a new one and leave the old one alone.
- 增用展开 [...prev, x],删用 filter(!==),改用 map(三元)。Add with spread [...prev, x], delete with filter and !==, edit with map and a conditional.
- map 长度不变、filter 可能变短、find 返回单个或 undefined。map keeps the length, filter can make it shorter, find returns one item or undefined.
- map 里用 async,外面一定要套 Promise.all。If you use async inside map, you must wrap the result in Promise.all.
- 「数据对但界面不动」是改了原对象的典型症状,而且不会报错。Right data with a frozen interface is the usual sign that you changed the original object, and nothing reports an error.