Notes Manager 增删改(React 考试 Q1)Notes Manager: add, edit and delete (React exam Q1)
题面The problem
先把要求读完,再动手。Read every requirement before you start.
Starting from an empty directory, build a React + TypeScript + Vite project. Implement add, delete and edit in Notes Manager, and make all four tests below pass. Do not open react-notes-app to look.
- 两个框都得有真实内容才能提交 —— 只有空格不算,按钮 disabledBoth fields need real content before you can submit. Spaces alone do not count, and the button stays disabled
- 新增追加到末尾,不是插到开头A new note is appended to the end of the list, not inserted at the top
- 提交成功后两个框都清空After a successful submit, both fields are cleared
- Delete 只删你点的那一条 —— 用 filter,不许 spliceDelete removes only the note you clicked. Use filter, not splice
- 点 Edit 把那条载进表单,按钮文字变成 UpdateClicking Edit loads that note into the form, and the button text becomes Update
- 编辑是原地替换 —— 位置不变、总数不变(「先删再加」会把它挪到末尾)Editing replaces the note in place: same position, same total count. Deleting it and adding it again would move it to the end
- 更新完表单清空,按钮变回 AddAfter the update the form is cleared and the button reads Add again
- 连着点两条的 Edit,表单要跟着换 —— 查 effect 的依赖写对没有Clicking Edit on one note and then on another must swap what the form shows. This checks that the effect has the right dependencies
预计 60 分钟。这个数字是照「读完题就开始写、不查资料」估的 —— 第一次超时很正常,第二次要压进去。Budget 60 minutes. Overrunning on the first pass is normal; the second pass should fit.
工作区Workspace
工作区是一个真的浏览器沙箱:左边写代码,右边实时预览,下面一个「跑测试」按钮。测试和本机那套是同一批断言,转写成了浏览器里能跑的写法。The workspace is a real in-browser sandbox: edit on the left, live preview on the right, one Run button below. The assertions are the same ones that pass on a real machine, rewritten for the browser runner.
需要联网。Requires an internet connection. 打包器和 npm 依赖都在 CodeSandbox 的远程服务上(评估过程见 docs/sandpack-evaluation.md),断网这块就起不来 —— 那就照下面的命令在本机跑。The bundler and the npm packages come from CodeSandbox's remote service, so this panel needs network access.
展开讲解Walkthrough
下面是《Task 1 · Add:提交表单,新笔记进入表格》那一节的原文 —— 和课程里是同一份内容,不是另写的摘要。卡住了再展开。Below is the actual text of the lesson “Task 1 · Add:提交表单,新笔记进入表格” — the same content as in the course, not a rewritten summary. Expand it when you stall.
展开《Task 1 · Add:提交表单,新笔记进入表格》(7 段 · 约 12 分钟)Expand “Task 1 · Add:提交表单,新笔记进入表格” (7 sections · ~12 min)
完整那一节(含练习、常见错误、迁移模式)在The full lesson (with exercises, common mistakes and transfer patterns) is at Task 1 · Add:提交表单,新笔记进入表格。
这一问在要求什么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参考答案Reference solution
提示是一级一级放的。四级看完还写不出来,再开答案门。The hints come one level at a time. If all four leave you stuck, open the answer.
这份答案在本机真跑过测试。但先确认你自己动手写过一遍 —— 读懂答案和写出答案是两种能力,考场上考的是后一种。This answer really was run here and its tests passed. But write it yourself first — reading an answer and producing one are two different skills, and the exam tests the second.