列表渲染与 keyRendering a list, and the key prop
notes.map(...) 那三行,以及为什么 key 不能用数组下标。Those three lines of notes.map(...), and why key must not be the array index.
这一页有什么On this page6
- 会用 map 把数组渲染成一串组件Use map to turn an array into a list of components
- 说清 key 是给谁看的、React 用它做什么Explain who key is for and what React does with it
- 知道为什么 key={index} 在有删除的列表里是错的Know why key={index} is wrong in a list that allows deletion
- 知道空列表要不要特殊处理Know whether an empty list needs special handling
Q1 的表格是 map 出来的,key 用错在这道题里会造成「删了一行,剩下的行内容串位」这种诡异现象 —— 而测试可能抓不到。The Q1 table is produced by map. A wrong key here makes rows show the wrong content after a delete, and the tests may not catch it.
react-notes-app/src/components/NoteTable/index.tsxmap 渲染 + keyRendered with map, and the key
react-notes-app/src/components/NoteTable/index.tsxmap 把「一串数据」变成「一串组件」map turns a list of data into a list of components
notes 是一个数组,页面上要出现对应数量的行。 做法就是 map —— 上一门课讲过它「长度不变、逐个变形」。 这里的变形是「一条 Note → 一个 <NoteItem />」。
注意 {notes.map(...)} 外面那对花括号: 这是「切回 JavaScript」。里面返回的是一个JSX 数组,React 会把它平铺渲染出来。
箭头函数的两种写法别搞混:(note) => (<NoteItem ... />) 用圆括号, 是「直接返回」;如果写成花括号(note) => { <NoteItem /> }, 那就是函数体,必须显式 return, 否则返回 undefined,页面上什么都不出现。
notes is an array, and the page needs one row per entry. The tool is map — the previous course described it as “same length, transform each item”. Here the transform is “one Note → one <NoteItem />”.
Notice the braces around {notes.map(...)}: that is the switch back into JavaScript. What comes out is a JSX array, and React renders it flat.
Do not mix up the two arrow-function forms:(note) => (<NoteItem ... />) with parentheses returns the element directly; written with braces, (note) => { <NoteItem /> }, that is a function body and you must return explicitly, or it returns undefined and nothing shows up on the page.
react-notes-app/src/components/NoteTable/index.tsxkey 是给 React 用来「认人」的key is how React tells one item from another
它不是给你看的,也不会出现在 DOM 里。It is not there for you to read, and it never appears in the DOM.
React 重新渲染时要做一次对比:「上一次的这一串,和这一次的这一串,谁是谁?」有了 key,它就能一一对应: key 还在的复用、key 消失的删掉、新 key 的插入。
没有 key,React 只能按位置猜, 并且给出警告:
key 的要求:在这一串里唯一、 并且跟着数据本身走。 这个项目用 note.id,正合适 —— id 是 Date.now() 生成的,每条唯一, 而且这条笔记不管排在第几位,id 都不变。
On every re-render React has to match one list against another:“of the row I had last time and the row I have now, which is which?” With a key it can pair them up one by one: keys that are still there get reused, keys that vanished get removed, new keys get inserted.
Without a key, React can only guess by position, and it tells you so:
What a key has to be: unique inside this list, and tied to the data itself. This project uses note.id, which fits — the id comes from Date.now(), so every note has its own, and it stays the same wherever the note sits in the list.
为什么 key={index} 是个陷阱Why key={index} goes wrong
在「只往后加」的列表里它没问题。一旦有删除或插入,就会串位。In a list that only grows at the end it is fine. As soon as you delete or insert, the rows get mismatched.
想一个具体场景。三条笔记,用下标做 key:
删掉中间那条之后,剩下两条重新 map,下标变成0、1。于是 React 看到的是: 「key=0 还在(内容变了?没变)、key=1 还在(内容从 B 变成 C)、 key=2 消失了」。
结果 React 会复用 key=1 那个组件实例, 只把里面的文字改掉。对纯展示组件影响不大, 但如果组件内部有自己的 state(比如一个展开/收起的开关、 一个未提交的输入框),那个 state 会留在原位置, 跟错误的数据配上对。
症状是「删了第二行,第三行的勾选状态跑到了第二行」这种, 极难查。所以规矩很简单:有稳定 id 就用 id,永远别用 index。
Picture a concrete case. Three notes, keyed by index:
Delete the middle one and the remaining two get mapped again, so the indexes become 0 and 1. What React sees is: “key=0 is still here (content changed? no), key=1 is still here (its content went from B to C), key=2 is gone”.
So React reuses the component instance behind key=1 and only swaps the text inside it. For a pure display component that barely matters, but if the component holds state of its own (an expand/collapse toggle, an unsubmitted input), that state stays with the position and gets paired with the wrong data.
The symptom reads like “I deleted the second row and the third row’s checkbox jumped up into it” — brutal to track down. So the rule is short: if there is a stable id, use the id. Never use index.
空列表需要特殊处理吗Does an empty list need special handling
不需要。[].map(...) 返回空数组, React 渲染空数组 = 什么都不渲染。 所以初始状态下 <tbody> 是空的, 表头照常显示。
这一点对测试很重要:测试里有一句expect(screen.getByTestId("notes-list")).not.toHaveTextContent("ToDelete")—— 它要求 notes-list 这个元素存在但不包含那段文字。如果你给空列表加个{notes.length === 0 ? <p>暂无</p> : <tbody>...</tbody>}这种分支,把 tbody 整个换掉了,getByTestId 就会找不到元素而抛错。
结论:不要动 data-testid 所在元素的存在性。这是 README 那条「不得修改任何 data-testid」的延伸含义。
No. [].map(...) returns an empty array, and rendering an empty array renders nothing. So <tbody> simply starts out empty while the header shows as usual.
That matters for the tests. One line reads expect(screen.getByTestId("notes-list")).not.toHaveTextContent("ToDelete")— it needs the notes-list element to exist while not containing that text. If you give the empty list a branch like{notes.length === 0 ? <p>Nothing yet</p> : <tbody>...</tbody>}and swap the whole tbody out, getByTestId finds nothing and throws.
So: never change whether an element carrying a data-testid exists. That is the wider meaning of the README line about not modifying any data-testid.
动手做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.
notes 里每条是 { id: number; title: string; content: string }, 用户可以删除任意一条。下面哪个是最合适的 key?
Each item in notes is { id: number; title: string; content: string }, and the user can delete any of them. Which of these is the best key?
下面哪一段会导致表格里一行都不出现(而且不报错)?
Which of these makes the table show no rows at all, without reporting an error?
换一道题也能用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.
- map 把数据数组变成 JSX 数组,React 会平铺渲染。map turns an array of data into an array of JSX, and React renders them one after another.
- key 是给 React 认人用的,要求「本串唯一 + 跟着数据走」。key is how React identifies each item. It must be unique inside that list and must stay with the data.
- 永远别用 index 当 key,更别用 Math.random()。Never use the index as key, and never use Math.random().
- 空数组 map 出空数组,不需要特殊处理 —— 也不要因此改动 testid 元素的结构。An empty array maps to an empty array, so no special handling is needed. Do not change the structure of the elements that carry a data-testid because of it.
- 箭头函数用花括号就必须 return,否则渲染空白且不报错。An arrow function written with curly braces needs return, or the list renders empty and no error appears.