DrillLab
第 11 / 21 节LESSON 11 / 21约 18 分钟~18 min

Task 3 · Edit:回填、改文字、就地更新、退出编辑Task 3 · Edit: refill the form, change the button text, update the row where it is, leave edit mode

四个要求串成一条链。这是整道 Q1 的压轴题。Four requirements linked into one chain. This is the hardest part of Q1.

3 个练习3 exercisesReact · 第 3 部分React · Part 3
这一页有什么On this page10
学完这节你会After this lesson you can
  • 独立写出 handleEdit 和 handleSubmitNote 的编辑分支Write handleEdit and the edit branch of handleSubmitNote on your own
  • 说清 noteToEdit 这一个 state 同时控制了哪四件事Say which four things the single noteToEdit state controls
  • 解释为什么必须复用旧 id,以及不复用会发生什么Explain why the old id has to be reused, and what happens if it is not
  • 解释为什么必须用 map 而不能「先删再加」Explain why map is required and why removing the item then adding it is not allowed
这在考试里考什么What the exam does with this

第 4 个测试查它,而且是四个测试里最长的一条。它同时验证「按钮文字变 Update」和「新内容替换旧内容」。「原位置」这个要求测试查不到,但它是题面明写的。The fourth test checks it, and it is the longest of the four. It verifies both that the button text becomes Update and that the new content replaces the old one. No test covers the in place requirement, but the task text states it clearly.

这节课要看的真实文件Real files this lesson looks at高亮行 = 需要你动手改的文件Highlighted rows are the files you edit
react-notes-app/src/components/NoteManager/index.tsxhandleEdit + handleSubmitNote 的 if 分支

提醒:源项目在磁盘上是做完的版本 —— 下面就是答案。想自己先写一遍的话,现在关上。Heads up: on disk this project is the finished version — what follows is the answer. Close this if you want to write it yourself first.

TSXindex.tsx源项目From source
1import { useState } from "react";
2import type { Note } from "../../types/Note";
3import NoteForm from "../NoteForm";
4import NoteTable from "../NoteTable";
5
6const NoteManager: React.FC = () => {
7 const [notes, setNotes] = useState<Note[]>([]);
8 const [noteToEdit, setNoteToEdit] = useState<Note | null>(null);
9
10 const handleSubmitNote = (submittedNote: Note) => {
11 if (noteToEdit) {
12 setNotes((prev) =>
13 prev.map((note) =>
14 note.id === submittedNote.id ? submittedNote : note,
15 ),
16 );
17 setNoteToEdit(null);
18 } else {
19 setNotes((prev) => [...prev, submittedNote]);
20 }
21 };
22
23 const handleDelete = (id: number) => {
24 setNotes((prev) => prev.filter((note) => note.id !== id));
25 };
26
27 const handleEdit = (note: Note) => {
28 setNoteToEdit(note);
29 };
30
31 return (
32 <div
33 className="layout-column align-items-center justify-content-start"
34 data-testid="note-manager"
35 >
36 <NoteForm onSubmit={handleSubmitNote} noteToEdit={noteToEdit} />
37 <NoteTable notes={notes} onDelete={handleDelete} onEdit={handleEdit} />
38 </div>
39 );
40};
41
42export default NoteManager;
Source: react-notes-app/src/components/NoteManager/index.tsx
react-notes-app/src/components/NoteForm/index.tsxuseEffect 回填 + id 复用 + 按钮文字(已给好)useEffect refills the form, the id is reused, and the button text changes (all given)
TSXindex.tsx源项目From source
1import React, { useState, useEffect } from "react";
2import type { Note } from "../../types/Note";
3
4interface NoteFormProps {
5 onSubmit: (note: Note) => void;
6 noteToEdit: Note | null;
7}
8
9const NoteForm: React.FC<NoteFormProps> = ({ onSubmit, noteToEdit }) => {
10 const [title, setTitle] = useState("");
11 const [content, setContent] = useState("");
12
13 useEffect(() => {
14 if (noteToEdit) {
15 setTitle(noteToEdit.title);
16 setContent(noteToEdit.content);
17 } else {
18 setTitle("");
19 setContent("");
20 }
21 }, [noteToEdit]);
22
23 const isFormInvalid = title.trim() === "" || content.trim() === "";
24
25 const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
26 event.preventDefault();
27 if (isFormInvalid) return;
28 const newNote = {
29 id: noteToEdit ? noteToEdit.id : Date.now(),
30 title: title.trim(),
31 content: content.trim(),
32 };
33 onSubmit(newNote);
34 setTitle("");
35 setContent("");
36 };
37
38 return (
39 <div className="card w-200 pt-30 pb-8 mt-15 mb-15">
40 <form onSubmit={handleSubmit} data-testid="note-form">
41 <section className="layout-row align-items-center justify-content-center mt-20 mr-20 ml-20">
42 <label className="form-title-label">Title:</label>
43 <input
44 type="text"
45 placeholder="Title"
46 value={title}
47 onChange={(e) => setTitle(e.target.value)}
48 data-testid="form-input"
49 className="form-input"
50 />
51 </section>
52
53 <section className="layout-row align-items-center justify-content-center mt-20 mr-20 ml-20">
54 <label className="form-content-label">Content:</label>
55 <textarea
56 placeholder="Content"
57 value={content}
58 onChange={(e) => setContent(e.target.value)}
59 data-testid="form-textarea"
60 className="form-textarea"
61 />
62 </section>
63
64 <section className="layout-row align-items-center justify-content-center mt-20 mr-20 ml-20">
65 <button
66 type="submit"
67 disabled={isFormInvalid}
68 data-testid="form-submit-button"
69 >
70 {noteToEdit ? "Update" : "Add"}
71 </button>
72 </section>
73 </form>
74 </div>
75 );
76};
77
78export default NoteForm;
Source: react-notes-app/src/components/NoteForm/index.tsx
§01

这一问在要求什么What this task asks for

原文:「点 Edit → 内容回填进表单、按钮变 Update → 提交 → 原位置更新该 note、退出编辑模式」。

六个要求,逐个找归属:

要求由谁实现要你写吗
点 Edit 进入编辑态handleEditsetNoteToEdit(note)
内容回填进表单NoteForm 的 useEffect(…, [noteToEdit])已给好
按钮变 UpdateNoteForm 的 {noteToEdit ? "Update" : "Add"}已给好
提交时复用旧 idNoteForm 的 id: noteToEdit ? noteToEdit.id : Date.now()已给好
原位置更新handleSubmitNotemap 分支
退出编辑模式setNoteToEdit(null)

所以真正要你写的只有三处,全在 NoteManager 里。 但你必须读懂已给好的那三处, 否则不知道自己写的东西为什么能生效。

The original: “click Edit → content filled back into the form, the button turns into Update → submit → update that note in place, leave edit mode”.

Six requirements. Find the owner of each:

RequirementWho implements itDo you write it?
Click Edit to enter edit modesetNoteToEdit(note) inside handleEditYes
Content filled back into the formNoteForm’s useEffect(…, [noteToEdit])Already given
The button turns into UpdateNoteForm’s {noteToEdit ? "Update" : "Add"}Already given
Reuse the old id on submitNoteForm’s id: noteToEdit ? noteToEdit.id : Date.now()Already given
Update in placeThe map branch of handleSubmitNoteYes
Leave edit modesetNoteToEdit(null)Yes

So only three spots are actually yours, all inside NoteManager. But you have to understand the three already given, otherwise you will not know why what you wrote works.

§02

noteToEdit 这一个 state,同时干了四件事One state, noteToEdit, does four jobs at the same time

这是这道题设计上最漂亮的地方。This is the neatest part of how the task is designed.

noteToEdit: Note | null 只是一个「当前正在编辑哪条」 的记录。但因为它被向下传给了 NoteForm, 它同时成了四件事的开关:

  1. 表单里显示什么 —— effect 依赖它,一变就回填。
  2. 按钮文字 —— 非 null 就是 Update。这个是渲染时同步算的, 所以点 Edit 的那一瞬间文字就变了。
  3. 提交时的 id —— 非 null 就复用它的 id。
  4. 提交进哪个分支 ——handleSubmitNoteif (noteToEdit)决定走 map 还是走追加。

所以 setNoteToEdit(null) 这一行也同时做了四件事的收尾: 清空表单、按钮变回 Add、下次提交生成新 id、下次提交走追加分支。漏了这一行,症状是「更新成功了,但表单还留着内容、 按钮还写着 Update,再改一次又更新同一条」。

noteToEdit: Note | null is just a record of which note is being edited right now. But because it gets passed down to NoteForm, it doubles as the switch for four things:

  1. What the form shows — the effect depends on it, so it refills the moment it changes.
  2. The button text — non-null means Update. This one is computed synchronously during render, so the text flips the instant you click Edit.
  3. The id used on submit — non-null means reuse its id.
  4. Which branch the submit takesif (noteToEdit) inside handleSubmitNote picks map or append.

So the single line setNoteToEdit(null) also closes out all four: clear the form, flip the button back to Add, generate a fresh id next time, take the append branch next time. Miss that line and the symptom is “the update worked, but the form still holds the text, the button still says Update, and editing again updates the same note”.

§03

把整条链走一遍Walk the whole chain once

六步。每一步都点开看。Six steps. Open each one and read it.

下面这张图把从「点 Edit」到「列表就地更新」的六步拆开了。 特别注意第 3 步和第 4 步的时间差 —— 按钮文字先变,输入框后填。

Task 3 的完整数据流:点 Edit → 回填 → 提交 → 就地更新第 1 / 6 步Step 1 of 6
① 点击
NoteItem 的 Edit 按钮
onEdit(note) —— 传整条
② 父组件
NoteManager.handleEdit
③ prop 下传
NoteForm 收到 noteToEdit
noteToEdit = null
④ 副作用
useEffect 回填两个 state
title = ""
⑤ 提交
handleSubmit 复用旧 id
⑥ 就地替换
handleSubmitNote 的 map 分支
notes = [A, B, C]
用户点了 B 那一行的 Edit。NoteItem 调用 onEdit(note)传的是整条 note, 不是 id —— 因为下游要用它的 title 和 content 回填。

The diagram below pulls apart the six steps from clicking Edit to the list updating in place. Watch the gap in time between step 3 and step 4 — the button text changes first, the inputs fill in after.

Task 3 的完整数据流:点 Edit → 回填 → 提交 → 就地更新第 1 / 6 步Step 1 of 6
① 点击
NoteItem 的 Edit 按钮
onEdit(note) —— 传整条
② 父组件
NoteManager.handleEdit
③ prop 下传
NoteForm 收到 noteToEdit
noteToEdit = null
④ 副作用
useEffect 回填两个 state
title = ""
⑤ 提交
handleSubmit 复用旧 id
⑥ 就地替换
handleSubmitNote 的 map 分支
notes = [A, B, C]
用户点了 B 那一行的 Edit。NoteItem 调用 onEdit(note)传的是整条 note, 不是 id —— 因为下游要用它的 title 和 content 回填。
§04

为什么必须复用旧 idWhy the old id has to be reused

这一行是整道题的枢纽。This one line is the center of the whole task.

NoteForm 里那行id: noteToEdit ? noteToEdit.id : Date.now(): 编辑时复用旧 id,新增时生成新 id。

为什么关键?因为 handleSubmitNote 的 map 分支靠 id 找那一条:note.id === submittedNote.id ? submittedNote : note。 如果提交上来的 note 带着一个全新的 id, 那 map 会走完整个数组、一个都匹配不上、 原样返回一份完全一样的新数组。

症状:点 Update 之后什么都没发生。没有报错,列表没变化。而且第 4 个测试会挂在最后两行 —— 既没出现 "New","Old" 也还在。

这也解释了 Task 1 那节提到的一个坑: 如果你在 handleSubmitNote 里自己给 note 重新生成 id, Task 1 照样能过,但 Task 3 会静默失败。两道题共享一个函数,一处多余的改动会跨题传染。

That line in NoteForm, id: noteToEdit ? noteToEdit.id : Date.now(): reuse the old id when editing, mint a new one when adding.

Why does it matter?Because the map branch of handleSubmitNote finds the right note by id: note.id === submittedNote.id ? submittedNote : note. If the submitted note arrives with a brand new id, map walks the whole array, matches nothing at all, and returns a new array with identical contents.

Symptom: clicking Update does nothing. No error, no change in the list. And the fourth test fails on its last two lines — "New" never appears and "Old" is still there.

This also explains the trap mentioned back in the Task 1 lesson: if you mint a new id for the note yourself inside handleSubmitNote, Task 1 keeps passing but Task 3 fails silently. Two tasks share one function, so one redundant change infects both.

§05

为什么必须用 map,不能「先删再加」Why map is required, and why removing the item then adding it is not

题目写的是「原位置更新」。 对比两种写法在三条笔记上的行为:

而第 4 个测试查不出这个区别 —— 它只有一条数据,谈不上顺序。 所以这又是一处「测试过了但没做对」。 判据只有 README 里那三个字。

map 的三元表达式读起来就是题目本身:「是那一条就换成新的,不是就原样留着」。 长度不变、顺序不变,这正是「原位置」的定义。

The brief says update in place. Compare how the two approaches behave on three notes:

And the fourth test cannot tell them apart — with one note there is no order to speak of. So here is another “the tests pass but it is not right”. The only judge is those two words in the README.

The ternary inside map reads like the brief itself:“if it is that one, swap in the new version; if not, leave it alone”. Same length, same order — which is exactly what “in place” means.

Text三种写法的实际结果What the three versions actually produce示意Illustrative
1初始:[A, B, C],编辑 B → B2
2
3✓ map 替换
4 prev.map(n => n.id === B.id ? B2 : n)
5 结果:[A, B2, C] ← B2 还在第二位,符合「原位置」
6
7✗ 先删再加
8 [...prev.filter(n => n.id !== B.id), B2]
9 结果:[A, C, B2] ← B2 跳到末尾,顺序变了
10
11✗ 先删再插到头部
12 [B2, ...prev.filter(...)]
13 结果:[B2, A, C] ← 也不是原位置
1start: [A, B, C], edit B → B2
2
3✓ replace with map
4 prev.map(n => n.id === B.id ? B2 : n)
5 result: [A, B2, C] ← B2 is still second, so it is in place
6
7✗ delete first, then append
8 [...prev.filter(n => n.id !== B.id), B2]
9 result: [A, C, B2] ← B2 jumped to the end, the order changed
10
11✗ delete first, then insert at the front
12 [B2, ...prev.filter(...)]
13 result: [B2, A, C] ← also not the original position
TSXsrc/components/NoteManager/index.tsx(map 分支)src/components/NoteManager/index.tsx (the map branch)源项目From source
1setNotes((prev) =>
2 prev.map((note) =>
3 note.id === submittedNote.id ? submittedNote : note,
4 ),
5);
Source: react-notes-app/src/components/NoteManager/index.tsx
§06

完整答案The complete answer

handleEdit 只有一行 —— 它不碰 notes, 因为编辑还没提交,列表不该变。这一点值得强调: 新手容易在 handleEdit 里就开始改列表。

handleSubmitNote 现在两个分支都齐了:

handleEdit is one line — it does not touch notes, because the edit has not been submitted yet and the list should not move. Worth stressing: beginners often start changing the list right there in handleEdit.

handleSubmitNote now has both branches:

TSXsrc/components/NoteManager/index.tsx(三道题的完整落点)src/components/NoteManager/index.tsx (where all three tasks land)源项目From source
1const handleSubmitNote = (submittedNote: Note) => {
2 if (noteToEdit) {
3 setNotes((prev) =>
4 prev.map((note) =>
5 note.id === submittedNote.id ? submittedNote : note,
6 ),
7 );
8 setNoteToEdit(null);
9 } else {
10 setNotes((prev) => [...prev, submittedNote]);
11 }
12};
13
14const handleEdit = (note: Note) => {
15 setNoteToEdit(note);
16};
Source: react-notes-app/src/components/NoteManager/index.tsx
§07

对应的测试,逐行读The matching test, read line by line

这是四个测试里最长的一条,它把整条链走了一遍:

  1. 3–5 行:先添加一条 Old / c1
  2. 7 行:点 Edit。注意这里用 getByRole 按文字 "Edit" 找按钮 —— 所以按钮文字不能改。
  3. 8 行:断言按钮文字变成 Update。 这条直接验证「按钮变 Update」。
  4. 10–12 行:clear 输入框再打 Newclear 能生效说明输入框必须是受控的, 而且 effect 必须已经把旧值填进去了(不然 clear 也没东西可清)。
  5. 14–15 行:断言列表里有 New 且没有 Old。 这验证「替换而不是新增」—— 如果你写成追加,Old 还在,第二条断言就挂。

This is the longest of the four tests, and it walks the whole chain:

  1. Lines 3-5: add one note, Old / c1.
  2. Line 7: click Edit. Note that it finds the button by the text "Edit" with getByRole — so that word cannot change.
  3. Line 8: assert the button text became Update. That checks “the button turns into Update” directly.
  4. Lines 10-12: clear the input, then type New. The fact that clear does something means the input has to be controlled, and the effect must already have put the old value in (otherwise there is nothing to clear).
  5. Lines 14-15: assert the list contains New and not Old. That checks “replace, not add” — write an append and Old is still there, so the second assertion fails.
TSXsrc/NoteManager.test.tsx(第 4 个测试)src/NoteManager.test.tsx (test 4)源项目From source
1test("edits a note in place", async () => {
2 render(<NoteManager />);
3 await userEvent.type(screen.getByTestId("form-input"), "Old");
4 await userEvent.type(screen.getByTestId("form-textarea"), "c1");
5 await userEvent.click(screen.getByTestId("form-submit-button"));
6
7 await userEvent.click(screen.getByRole("button", { name: "Edit" }));
8 expect(screen.getByTestId("form-submit-button")).toHaveTextContent("Update");
9
10 const input = screen.getByTestId("form-input");
11 await userEvent.clear(input);
12 await userEvent.type(input, "New");
13 await userEvent.click(screen.getByTestId("form-submit-button"));
14
15 expect(screen.getByTestId("notes-list")).toHaveTextContent("New");
16 expect(screen.getByTestId("notes-list")).not.toHaveTextContent("Old");
17});
Source: react-notes-app/src/NoteManager.test.tsx
练习Practice

动手做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.

L2填空Fill the blanks补全编辑逻辑的四个关键位置Fill in the four key spots of the edit logic

四个空横跨两个函数。第 4 个空是最容易漏的那一行 —— 漏了它测试照样能过,但行为明显不对。

Four blanks across two functions. The fourth is the line people forget most often — without it the tests still pass, but the behavior is clearly wrong.

TSXsrc/components/NoteManager/index.tsx4 个空4 blanks
1const handleSubmitNote = (submittedNote: Note) => {
2 if (noteToEdit) {
3 setNotes((prev) =>
4 prev.((note) =>
5 note.id submittedNote.id ? : note,
6 ),
7 );
8 ;
9 } else {
10 setNotes((prev) => [...prev, submittedNote]);
11 }
12};
13
14const handleEdit = (note: Note) => {
15 setNoteToEdit(note);
16};
把 4 个空都填上才能检查(还差 4 个)Fill all 4 blanks to check (4 to go)
L3写整块Write a block不看答案,自己写出完整的 Task 3Write all of Task 3 yourself, without looking at the answer

handleEdithandleSubmitNote两个函数完整写出来(含 Task 1 的分支)。 这是 Q1 的完整答案,写对了这道题就通了。

Write both handleEdit and handleSubmitNote in full, including the Task 1 branch. This is the complete answer to Q1: get it right and the question is done.

要求Requirements
  • handleEdit:把这条笔记设为「正在编辑」,不要改动 noteshandleEdit: mark this note as the one being edited, and leave notes alone
  • handleSubmitNote 编辑分支:按 id 就地替换,位置和顺序不变handleSubmitNote, edit branch: replace by id in place, keeping the position and the order
  • handleSubmitNote 编辑分支:替换完要退出编辑模式handleSubmitNote, edit branch: leave edit mode once the replace is done
  • handleSubmitNote 新增分支:追加到末尾handleSubmitNote, add branch: append to the end
  • 全部使用函数式更新,不许修改原数组Use functional updates everywhere, and never change the original array
TSXsrc/components/NoteManager/index.tsx
This check is textual: it looks for the right constructs, it does not run your code
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.

L3Debug LabDebug LabDebug Lab · 点 Update 之后毫无反应Debug Lab · nothing happens after you click Update

点 Edit,输入框正常回填,按钮变成 Update。 改完内容点 Update —— 列表一点变化都没有, 表单也没清空。控制台干净。

Click Edit and the inputs prefill correctly, and the button becomes Update. Change the content and click Update — the list does not change at all, and the form does not clear either. The console is clean.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
# 没有任何报错。 # 复现: # 1. 添加 "Old / c1" # 2. 点 Edit → 输入框显示 Old / c1,按钮变 Update ✓ 这两步正常 # 3. 把标题改成 "New",点 Update # 期望:列表里那条变成 New,表单清空,按钮回到 Add # 实际:列表还是 Old,表单还留着 New,按钮还是 Update # 测试结果: # ✓ adds a note # ✓ submit button disabled when inputs empty # ✓ deletes a note # ✕ edits a note in place # Unable to find text content "New" in element [data-testid="notes-list"]# No error at all. # Repro: # 1. Add "Old / c1" # 2. Click Edit → the inputs show Old / c1, the button reads Update ✓ both fine # 3. Change the title to "New" and click Update # Expected: that row becomes New, the form clears, the button goes back to Add # Actual: the row is still Old, the form still holds New, the button still reads Update # Test results: # ✓ adds a note # ✓ submit button disabled when inputs empty # ✓ deletes a note # ✕ edits a note in place # Unable to find text content "New" in element [data-testid="notes-list"]
TSX有问题的 handleSubmitNoteThe handleSubmitNote with the bug示意Illustrative
1const handleSubmitNote = (submittedNote: Note) => {
2 const note = { ...submittedNote, id: Date.now() };
3
4 if (noteToEdit) {
5 setNotes((prev) =>
6 prev.map((n) => (n.id === note.id ? note : n)),
7 );
8 setNoteToEdit(null);
9 } else {
10 setNotes((prev) => [...prev, note]);
11 }
12};
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
错例Wrong

初学者常见的几种写法错误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.

TSX示意Illustrative
1// ✗ handleEdit 里就开始改列表
2const handleEdit = (note: Note) => {
3 setNoteToEdit(note);
4 setNotes((prev) => prev.filter((n) => n.id !== note.id)); // 先把它删掉?
5};
1// ✗ handleEdit starts changing the list already
2const handleEdit = (note: Note) => {
3 setNoteToEdit(note);
4 setNotes((prev) => prev.filter((n) => n.id !== note.id)); // remove it first?
5};
点 Edit 只是「进入编辑状态」,用户还没提交, 甚至可能改完就放弃了。这里把它删掉, 等于「点 Edit 就丢数据」。handleEdit 唯一的职责是 setNoteToEdit。Clicking Edit only enters edit mode. The user has not submitted anything yet, and may give up halfway. Removing the note here means that clicking Edit loses data.The only job of handleEdit is setNoteToEdit.
TSX示意Illustrative
1// ✗ 忘了退出编辑模式
2if (noteToEdit) {
3 setNotes((prev) => prev.map((n) => (n.id === submittedNote.id ? submittedNote : n)));
4 // 少了 setNoteToEdit(null);
5}
1// ✗ Forgetting to leave edit mode
2if (noteToEdit) {
3 setNotes((prev) => prev.map((n) => (n.id === submittedNote.id ? submittedNote : n)));
4 // setNoteToEdit(null); is missing
5}
第 4 个测试照样会过 —— 它在提交后只检查了列表内容,没检查表单和按钮。 但行为是坏的:表单还留着刚才的内容、按钮还写着 Update、 再点一次提交还是在更新同一条。
题目原文有「退出编辑模式」四个字,这是明确要求。
The fourth test still passes, because after the submit it only checks the list contents, not the form and not the button. But the behavior is wrong: the form still holds the previous text, the button still says Update, and submitting again updates the same note.
The task text says to leave edit mode. That is a stated requirement.
TSX示意Illustrative
1// ✗ 用「先删再加」实现更新
2if (noteToEdit) {
3 setNotes((prev) => [
4 ...prev.filter((n) => n.id !== submittedNote.id),
5 submittedNote,
6 ]);
7 setNoteToEdit(null);
8}
1// ✗ Doing the update as a delete followed by an add
2if (noteToEdit) {
3 setNotes((prev) => [
4 ...prev.filter((n) => n.id !== submittedNote.id),
5 submittedNote,
6 ]);
7 setNoteToEdit(null);
8}
测试会过(只有一条数据),但被编辑的那条会跳到列表末尾,违反题目明写的「原位置」。
验证方法:手动加三条,编辑中间那条,看它是否还在第二行。
The test passes, because there is only one note, but the edited notemoves to the end of the list, which breaks the in place requirement stated in the task.
How to check: add three notes by hand, edit the middle one, and see whether it is still on the second row.
迁移Transfer

换一道题也能用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.

「更新某一条,位置不变」Update one item without moving it
map + 三元,用 === 匹配map plus a ternary, matching with ===
「进入编辑态 / 选中某一项」Enter edit mode, or select one item
一个 selected: T | null 的 stateOne state of type selected: T | null
「点了更新但毫无反应」Update was clicked and nothing happened
查匹配用的 id 是不是被改过Check whether the id used for matching was changed
「更新完要恢复初始态」Go back to the starting state after the update
把那个 T | null 的 state 设回 nullSet that T | null state back to null
一个函数服务两种模式One function serves two modes
改动前把所有分支都想一遍Think through every branch before you change it
这节的要点What to take away
  1. noteToEdit 一个 state 控制四件事:回填、按钮文字、提交时的 id、提交走哪个分支。The single noteToEdit state controls four things: filling the form, the button text, the id used on submit, and which branch the submit takes.
  2. handleEdit 只 setNoteToEdit(note),绝不碰 notes。handleEdit only calls setNoteToEdit(note). It never touches notes.
  3. 编辑分支用 map + === 就地替换,长度和顺序都不变。The edit branch uses map and === to replace the item in place, so the length and the order stay the same.
  4. 必须复用旧 id,否则 map 匹配不上,更新静默失败。The old id has to be reused, or map finds no match and the update fails without any message.
  5. setNoteToEdit(null) 不能漏 —— 测试查不到,但题目明写了「退出编辑模式」。Do not forget setNoteToEdit(null). No test checks it, but the task text does say to leave edit mode.

接下来What next

  1. 把这一节的练习做掉Do this lesson’s exercises3 个,就在这一页上面 —— 别攒着最后一起做3 of them, further up this page — do not save them for later
    回到练习 ↑Back up to them ↑
  2. 接着看下一节Continue to the next lesson四个测试逐条读,以及它们的盲区The four tests read line by line, and what they fail to catch
    下一节Next lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: Task 2 · Delete:点 Delete,该行按 id 被移除Task 2 · Delete: click Delete and that one row is removed by id