Task 1 · Add:提交表单,新笔记进入表格Task 1 · Add: submit the form and the new note appears in the table
三道题里最简单的一道,但它建立了后两道题的全部结构。The easiest of the three tasks, but it sets up the whole structure the other two use.
这一页有什么On this page10
- 01 这一问在要求什么What this task asks for
- 02 这一问真正考什么What this task is really testing
- 03 先看现有代码:note 是谁造的Look at the existing code first: who builds the note
- 04 先想再写Think it through before you write
- 05 分步实现Build it step by step
- 06 为什么这样就成立了Why this works
- 07 对应的测试The matching test
- 练习 · 动手做Practice
- 常见错误Common mistakes
- 迁移模式Transfer
- 独立写出 handleSubmitNote 的新增分支Write the add branch of handleSubmitNote on your own
- 说清 note 是在哪里被构造出来的、id 从哪来Say where the note object is built and where its id comes from
- 解释为什么这里必须造新数组Explain why a new array is required here
- 知道对应的测试在断言什么Know what the matching test asserts
第 1 个测试直接查它。而且它确立了「子组件 onSubmit 上报 → 父组件改 notes」这条链 —— Task 3 的后半复用同一个函数。The first test checks it directly. It also sets up the chain where the child reports through onSubmit and the parent changes notes. The second half of Task 3 reuses the same function.
react-notes-app/src/components/NoteManager/index.tsxhandleSubmitNote 的 else 分支
提醒:源项目在磁盘上是做完的版本 —— 下面就是答案。想自己先写一遍的话,现在关上。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.tsxnote 在这里被构造并上报The note is built here and reported upwards
react-notes-app/src/components/NoteForm/index.tsx这一问在要求什么What this task asks for
「提交表单 → 新 note 进入表格」。拆开是三件事:
- 用户在两个输入框里填好内容,点 Add。
- 产生一条完整的
Note(含一个唯一 id)。 - 这条 note 出现在表格里,原有的都还在。
第 2 件事已经由 NoteForm 做完了(上一模块读过它)。 所以你要写的只有第 3 件事。
“Submit the form → the new note enters the table”. Split open, that is three things:
- The user fills in both inputs and clicks Add.
- A complete
Notecomes into being, with a unique id. - That note shows up in the table, and everything already there stays.
The second one is already done by NoteForm (you read it in the previous module). So the only thing left for you is the third.
这一问真正考什么What this task is really testing
表面上是「往数组里加一条」。真正考的是三点:
- 你知不知道数据该往哪存。notes 在
NoteManager,不在NoteForm。 - 你会不会不可变更新。
push能让数组变长,但界面不动。 - 你能不能顺着 props 找到调用链。
NoteForm的onSubmit这个 prop 是从哪来的、传的是谁。
On the surface it is “append one item to an array”. What it really tests is three things:
- Whether you know where the data belongs. notes lives in
NoteManager, notNoteForm. - Whether you can do an immutable update.
pushmakes the array longer, but the screen never moves. - Whether you can follow props to the call chain. Where
NoteForm’sonSubmitprop comes from, and what gets passed into it.
先看现有代码:note 是谁造的Look at the existing code first: who builds the note
NoteForm 已经把整条 note 造好了,包括 id。NoteForm already builds the whole note, id included.
handleSubmit(在 NoteForm 里) 做了这几件事,其中第 3 步就是构造 note:
注意 id: noteToEdit ? noteToEdit.id : Date.now(): 新增时 noteToEdit 是 null, 所以走 Date.now() —— 当前毫秒时间戳, 作为 id 足够唯一(同一毫秒内连点两次才会撞,实际上做不到)。
还有 .trim():前后空格被去掉了。 所以传到 NoteManager 的一定是干净的数据 —— 你不需要再校验一遍。
然后 onSubmit(newNote) 把它交出去。这个 onSubmit 是从 props 来的, 而 NoteManager 传给它的正是handleSubmitNote:
handleSubmit (over in NoteForm) does a handful of things, and step 3 is where the note gets built:
Look at id: noteToEdit ? noteToEdit.id : Date.now(): when adding, noteToEdit is null, so it takes Date.now() — the current millisecond timestamp, unique enough for an id (you would have to click twice inside one millisecond to collide, which you cannot).
And .trim(): the surrounding spaces are gone. So what reaches NoteManager is always clean data — you do not have to validate it again.
Then onSubmit(newNote) hands it off. That onSubmit comes from props, and what NoteManager passes in is exactly handleSubmitNote:
react-notes-app/src/components/NoteForm/index.tsxreact-notes-app/src/components/NoteManager/index.tsx先想再写Think it through before you write
下面五个问题都能答上来,代码自然就出来了。Once you can answer these five questions, the code follows.
第 3 个问题的答案「旧的全部保留,末尾多一条」, 翻译成代码就是 [...prev, submittedNote]。 展开语法把旧数组的每个元素铺开,后面接上新的。
The answer to question 3, “everything old stays, one more at the end”, turns straight into [...prev, submittedNote]. Spread lays out every element of the old array, and the new one goes after them.
分步实现Build it step by step
第一步:先只考虑新增,不管编辑。(编辑是 Task 3 的事,现在假装没有。)
第二步:改成函数式更新。setNotes([...notes, submittedNote]) 也能过测试, 但项目里统一用 prev 形式。理由在上一模块讲过: 连续调用时不会拿到过期的值。
第三步:给编辑留位置。Task 3 会在这个函数里加一个分支。 所以最终形态是 if (noteToEdit) {...} else { 新增 }。 现在先只写 else 那半边。
Step one: think about adding only, ignore editing. (Editing belongs to Task 3. Pretend it does not exist yet.)
Step two: switch to the functional update.setNotes([...notes, submittedNote]) passes the tests too, but the project uses the prev form throughout. The reason came up in the previous module: back-to-back calls never read a stale value.
Step three: leave room for editing. Task 3 adds a branch inside this same function. So the final shape is if (noteToEdit) {...} else { add }. For now write only the else half.
react-notes-app/src/components/NoteManager/index.tsx为什么这样就成立了Why this works
逐段看这一行 setNotes((prev) => [...prev, submittedNote]):
setNotes(...)—— 唯一合法的修改途径。 调用它 = 告诉 React「值变了,请重新渲染」。(prev) => ...—— 函数式更新。 React 会把「此刻最新的 notes」作为prev传进来。[...prev, submittedNote]—— 一个全新的数组。旧元素逐个铺开,新的接在末尾。 因为是新数组,React 能看出变化。
之后就是上一模块那张图的流程:state 更新 → NoteManager重新执行 → notes 多了一条 →NoteTable 收到新数组 →notes.map(...) 多产出一个 NoteItem → React 对比后往 DOM 里插一个 <tr>。
Read that one line piece by piece — setNotes((prev) => [...prev, submittedNote]):
setNotes(...)— the only legal way to change it. Calling it means telling React “the value changed, please re-render”.(prev) => ...— the functional update. React hands you the newest notes at this moment asprev.[...prev, submittedNote]— a brand new array. The old items are laid out one by one and the new one is attached at the end. Because it is a new array, React can see the change.
After that it is the flow from the diagram in the previous module: state updates → NoteManager runs again → notes has one more item → NoteTable receives the new array → notes.map(...) produces one more NoteItem → React diffs and inserts one <tr> into the DOM.
对应的测试The matching test
userEvent.type 模拟真人打字(一个字符一个字符触发 onChange),userEvent.click 模拟点击。 最后断言 notes-list 这个元素的文字内容里含My Title。
注意这个测试的宽松之处:它只检查「文字出现了」。所以就算你把新笔记加在开头([submittedNote, ...prev]), 这个测试照样过。但题目说的是「进入表格」, 常规理解是追加到末尾 —— 而且真实答案就是追加。别因为测试宽松就随便写。
userEvent.type types like a real person (one character at a time, each firing onChange), and userEvent.click clicks. The last line asserts that the text content of the notes-list element contains My Title.
Notice how loose this test is: it only checks that the text showed up. So even if you put the new note at the front ([submittedNote, ...prev]), it still passes. But the brief says “enters the table”, which normally means appending to the end — and the real answer does append. A loose test is no excuse for a sloppy answer.
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.
两个空。想清楚「旧的要不要留」和「用哪种更新形式」。
Two blanks. Decide whether the old notes have to stay, and which form of update to use.
只给你函数签名和要求。自己写完整实现。 写完点「检查我的代码」,它会用文本规则检查你有没有踩坑。
You get the function signature and the requirements. Write the whole implementation yourself. When you are done, use the check button: it applies text rules to see whether you fell into a known trap.
- 把 submittedNote 追加到 notes 的末尾Append submittedNote to the end of notes
- 原有的笔记全部保留Keep every note that was already there
- 必须用函数式更新 setNotes(prev => ...)Use a functional update: setNotes(prev => ...)
- 不许用 push / splice / unshift 修改原数组Do not change the original array with push / splice / unshift
- 留出 if (noteToEdit) 分支的位置(Task 3 会填)Leave room for the if (noteToEdit) branch (Task 3 fills it in)
看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。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.
初学者常见的几种写法错误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.
NoteTable 拿不到 NoteForm 内部的 state —— 它们是兄弟。这份 myNotes 白存了。数据只能放在共同祖先。NoteTable cannot reach state that lives insideNoteForm — they are siblings. This myNotesis stored for nothing. The data can only live in a shared parent.[[...旧数组], 新note] —— 一个嵌套数组。 TypeScript 会报Type 'Note[]' is not assignable to type 'Note'。 好在这个错编译期就被抓到了。This builds [[...old array], new note], an array inside an array. TypeScript reportsType 'Note[]' is not assignable to type 'Note'. At least this mistake is caught at compile time.NoteForm 已经给好 id 了,这里再造一个纯属多余。 更糟的是它会直接毁掉 Task 3 —— 编辑提交时 NoteForm 特意复用了旧 id, 而这行会把它覆盖成新 id,于是 map 找不到匹配项, 更新静默失败(列表毫无变化)。NoteForm already set the id, so building another one here is pointless. Worse, it breaks Task 3: on an edit submit NoteForm deliberately reuses the old id, and this line overwrites it with a new one. Then map finds no match and the update fails silently, with no change in the list.换一道题也能用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.
- note 由 NoteForm 构造(含 id 和 trim),NoteManager 只负责存。NoteForm builds the note, including the id and the trim. NoteManager only stores it.
- onSubmit 这个 prop 接的就是 handleSubmitNote —— 顺着 props 能找到调用链。The onSubmit prop receives handleSubmitNote. Follow the props and you find the call chain.
- setNotes(prev => [...prev, note]) 是标准写法:新数组、旧的全留、新的在末尾。setNotes(prev => [...prev, note]) is the standard form: a new array, every old item kept, the new one at the end.
- 不要在 handleSubmitNote 里重新生成 id,那会毁掉 Task 3。Do not generate a new id inside handleSubmitNote. That breaks Task 3.
- 第 1 个测试只查文字出现,比题目要求宽松 —— 别因此偷懒。The first test only checks that the text appears, which is looser than the task requires. Do not do less because of that.