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.
这一页有什么On this page10
- 01 这一问在要求什么What this task asks for
- 02 noteToEdit 这一个 state,同时干了四件事One state, noteToEdit, does four jobs at the same time
- 03 把整条链走一遍Walk the whole chain once
- 04 为什么必须复用旧 idWhy the old id has to be reused
- 05 为什么必须用 map,不能「先删再加」Why map is required, and why removing the item then adding it is not
- 06 完整答案The complete answer
- 07 对应的测试,逐行读The matching test, read line by line
- 练习 · 动手做Practice
- 常见错误Common mistakes
- 迁移模式Transfer
- 独立写出 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
第 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.
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.
react-notes-app/src/components/NoteManager/index.tsxreact-notes-app/src/components/NoteForm/index.tsxuseEffect 回填 + id 复用 + 按钮文字(已给好)useEffect refills the form, the id is reused, and the button text changes (all given)
react-notes-app/src/components/NoteForm/index.tsx这一问在要求什么What this task asks for
原文:「点 Edit → 内容回填进表单、按钮变 Update → 提交 → 原位置更新该 note、退出编辑模式」。
六个要求,逐个找归属:
| 要求 | 由谁实现 | 要你写吗 |
|---|---|---|
| 点 Edit 进入编辑态 | handleEdit 里 setNoteToEdit(note) | 要 |
| 内容回填进表单 | NoteForm 的 useEffect(…, [noteToEdit]) | 已给好 |
| 按钮变 Update | NoteForm 的 {noteToEdit ? "Update" : "Add"} | 已给好 |
| 提交时复用旧 id | NoteForm 的 id: noteToEdit ? noteToEdit.id : Date.now() | 已给好 |
| 原位置更新 | handleSubmitNote 的 map 分支 | 要 |
| 退出编辑模式 | 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:
| Requirement | Who implements it | Do you write it? |
|---|---|---|
| Click Edit to enter edit mode | setNoteToEdit(note) inside handleEdit | Yes |
| Content filled back into the form | NoteForm’s useEffect(…, [noteToEdit]) | Already given |
| The button turns into Update | NoteForm’s {noteToEdit ? "Update" : "Add"} | Already given |
| Reuse the old id on submit | NoteForm’s id: noteToEdit ? noteToEdit.id : Date.now() | Already given |
| Update in place | The map branch of handleSubmitNote | Yes |
| Leave edit mode | setNoteToEdit(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.
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, 它同时成了四件事的开关:
- 表单里显示什么 —— effect 依赖它,一变就回填。
- 按钮文字 —— 非 null 就是 Update。这个是渲染时同步算的, 所以点 Edit 的那一瞬间文字就变了。
- 提交时的 id —— 非 null 就复用它的 id。
- 提交进哪个分支 ——
handleSubmitNote里if (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:
- What the form shows — the effect depends on it, so it refills the moment it changes.
- The button text — non-null means Update. This one is computed synchronously during render, so the text flips the instant you click Edit.
- The id used on submit — non-null means reuse its id.
- Which branch the submit takes —
if (noteToEdit)insidehandleSubmitNotepicks 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”.
把整条链走一遍Walk the whole chain once
六步。每一步都点开看。Six steps. Open each one and read it.
下面这张图把从「点 Edit」到「列表就地更新」的六步拆开了。 特别注意第 3 步和第 4 步的时间差 —— 按钮文字先变,输入框后填。
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.
NoteItem 调用 onEdit(note),传的是整条 note, 不是 id —— 因为下游要用它的 title 和 content 回填。为什么必须复用旧 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.
为什么必须用 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.
react-notes-app/src/components/NoteManager/index.tsx完整答案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:
react-notes-app/src/components/NoteManager/index.tsx对应的测试,逐行读The matching test, read line by line
这是四个测试里最长的一条,它把整条链走了一遍:
- 3–5 行:先添加一条
Old / c1。 - 7 行:点 Edit。注意这里用 getByRole 按文字 "Edit" 找按钮 —— 所以按钮文字不能改。
- 8 行:断言按钮文字变成
Update。 这条直接验证「按钮变 Update」。 - 10–12 行:
clear输入框再打New。clear 能生效说明输入框必须是受控的, 而且 effect 必须已经把旧值填进去了(不然 clear 也没东西可清)。 - 14–15 行:断言列表里有
New且没有Old。 这验证「替换而不是新增」—— 如果你写成追加,Old还在,第二条断言就挂。
This is the longest of the four tests, and it walks the whole chain:
- Lines 3-5: add one note,
Old / c1. - Line 7: click Edit. Note that it finds the button by the text "Edit" with getByRole — so that word cannot change.
- Line 8: assert the button text became
Update. That checks “the button turns into Update” directly. - Lines 10-12:
clearthe input, then typeNew. 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). - Lines 14-15: assert the list contains
Newand notOld. That checks “replace, not add” — write an append andOldis still there, so the second assertion fails.
react-notes-app/src/NoteManager.test.tsx动手做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.
四个空横跨两个函数。第 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.
把 handleEdit 和 handleSubmitNote两个函数完整写出来(含 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.
- 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
看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。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.
点 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.
初学者常见的几种写法错误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.
题目原文有「退出编辑模式」四个字,这是明确要求。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.
验证方法:手动加三条,编辑中间那条,看它是否还在第二行。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.
换一道题也能用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.
- 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.
- handleEdit 只 setNoteToEdit(note),绝不碰 notes。handleEdit only calls setNoteToEdit(note). It never touches notes.
- 编辑分支用 map + === 就地替换,长度和顺序都不变。The edit branch uses map and === to replace the item in place, so the length and the order stay the same.
- 必须复用旧 id,否则 map 匹配不上,更新静默失败。The old id has to be reused, or map finds no match and the update fails without any message.
- setNoteToEdit(null) 不能漏 —— 测试查不到,但题目明写了「退出编辑模式」。Do not forget setNoteToEdit(null). No test checks it, but the task text does say to leave edit mode.