DrillLab

Notes Manager 增删改(React 考试 Q1)Notes Manager: add, edit and delete (React exam Q1)

React困难 · Hard约 60 分钟~60 min浏览器里能跑Runs in the browser
§01

题面The problem

先把要求读完,再动手。Read every requirement before you start.

在下面的工作区里实现 Notes Manager 的增删改,让八个测试全过。两个文件要自己写:NoteManager.tsx(数据和三个 handler)和 NoteForm.tsx(受控输入、校验、编辑态同步)。types.ts / NoteItem.tsx / NoteTable.tsx 给定,不用改。想练「连项目一起从零搭」,去考场那一版。

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.

验收标准Acceptance criteria
  • 两个框都得有真实内容才能提交 —— 只有空格不算,按钮 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.

§02

工作区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.

6 个起始文件 · 目标 8 passed6 starter files · target 8 passed
自己写出来、测试全绿之后再打勾。看懂答案不算。Tick this only after you wrote it yourself and the tests went green.
§03

展开讲解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:提交表单,新笔记进入表格

§01

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

「提交表单 → 新 note 进入表格」。拆开是三件事:

  1. 用户在两个输入框里填好内容,点 Add。
  2. 产生一条完整的 Note(含一个唯一 id)。
  3. 这条 note 出现在表格里,原有的都还在

第 2 件事已经由 NoteForm 做完了(上一模块读过它)。 所以你要写的只有第 3 件事。

“Submit the form → the new note enters the table”. Split open, that is three things:

  1. The user fills in both inputs and clicks Add.
  2. A complete Note comes into being, with a unique id.
  3. 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.

§02

这一问真正考什么What this task is really testing

表面上是「往数组里加一条」。真正考的是三点:

  • 你知不知道数据该往哪存。notes 在 NoteManager,不在 NoteForm
  • 你会不会不可变更新。push 能让数组变长,但界面不动。
  • 你能不能顺着 props 找到调用链。NoteFormonSubmit 这个 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, not NoteForm.
  • Whether you can do an immutable update.push makes the array longer, but the screen never moves.
  • Whether you can follow props to the call chain. Where NoteForm’s onSubmit prop comes from, and what gets passed into it.
§03

先看现有代码: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(): 新增时 noteToEditnull, 所以走 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:

TSXsrc/components/NoteForm/index.tsx(节选)src/components/NoteForm/index.tsx (excerpt)源项目From source
1// NoteForm 里:构造并上报
2const newNote = {
3 id: noteToEdit ? noteToEdit.id : Date.now(),
4 title: title.trim(),
5 content: content.trim(),
6};
7onSubmit(newNote);
1// Inside NoteForm: build the note and report it up
2const newNote = {
3 id: noteToEdit ? noteToEdit.id : Date.now(),
4 title: title.trim(),
5 content: content.trim(),
6};
7onSubmit(newNote);
Source: react-notes-app/src/components/NoteForm/index.tsx
TSXsrc/components/NoteManager/index.tsx(节选)src/components/NoteManager/index.tsx (excerpt)源项目From source
1// NoteManager 里:把 handleSubmitNote 接上去
2<NoteForm onSubmit={handleSubmitNote} noteToEdit={noteToEdit} />
1// Inside NoteManager: wire handleSubmitNote up
2<NoteForm onSubmit={handleSubmitNote} noteToEdit={noteToEdit} />
Source: react-notes-app/src/components/NoteManager/index.tsx
所以 NoteForm 里那句 onSubmit(newNote),实际执行的是 NoteManager 里的 handleSubmitNote(newNote)。这就是「props 传函数」这条链的全貌。So the line onSubmit(newNote) inside NoteForm really runs handleSubmitNote(newNote) inside NoteManager. That is the whole chain of passing a function through props.
§04

先想再写Think it through before you write

下面五个问题都能答上来,代码自然就出来了。Once you can answer these five questions, the code follows.

先别写代码 · 先回答这几个问题Before you write code · answer these first
1.输入是什么?—— 一条已经构造好、已经 trim 过的 Note。
2.输出是什么?—— notes 这个 state 的新值。
3.新值和旧值什么关系?—— 旧的全部保留,末尾多一条。
4.谁负责改?—— 持有 notes 的组件,也就是 NoteManager。
5.能不能直接改旧数组?—— 不能。React 靠「是不是同一个对象」判断变化。

第 3 个问题的答案「旧的全部保留,末尾多一条」, 翻译成代码就是 [...prev, submittedNote]。 展开语法把旧数组的每个元素铺开,后面接上新的。

先别写代码 · 先回答这几个问题Before you write code · answer these first
1.What is the input? A Note that is already built and already trimmed.
2.What is the output? The new value of the notes state.
3.How does the new value relate to the old one? Everything old stays, one more at the end.
4.Who is allowed to change it? The component that holds notes, i.e. NoteManager.
5.Can you edit the old array directly? No. React decides whether something changed by asking whether it is the same object.

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.

§05

分步实现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.

TSX推导过程 · 第一步Working it out · step one示意Illustrative
1// 第一步:最直白的写法
2const handleSubmitNote = (submittedNote: Note) => {
3 setNotes([...notes, submittedNote]);
4};
1// Step one: the most direct version
2const handleSubmitNote = (submittedNote: Note) => {
3 setNotes([...notes, submittedNote]);
4};
TSXsrc/components/NoteManager/index.tsx源项目From source
1// 最终形态(Task 1 的那半边)
2const handleSubmitNote = (submittedNote: Note) => {
3 if (noteToEdit) {
4 // Task 3 会填这里
5 } else {
6 setNotes((prev) => [...prev, submittedNote]);
7 }
8};
1// The final shape (the half that belongs to Task 1)
2const handleSubmitNote = (submittedNote: Note) => {
3 if (noteToEdit) {
4 // Task 3 fills this in
5 } else {
6 setNotes((prev) => [...prev, submittedNote]);
7 }
8};
Source: react-notes-app/src/components/NoteManager/index.tsx
§06

为什么这样就成立了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 as prev.
  • [...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.

§07

对应的测试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.

TSXsrc/NoteManager.test.tsx(第 1 个测试)src/NoteManager.test.tsx (test 1)源项目From source
1test("adds a note", async () => {
2 render(<NoteManager />);
3 await userEvent.type(screen.getByTestId("form-input"), "My Title");
4 await userEvent.type(screen.getByTestId("form-textarea"), "My Content");
5 await userEvent.click(screen.getByTestId("form-submit-button"));
6
7 expect(screen.getByTestId("notes-list")).toHaveTextContent("My Title");
8});
Source: react-notes-app/src/NoteManager.test.tsx
§04

参考答案Reference solution

提示是一级一级放的。四级看完还写不出来,再开答案门。The hints come one level at a time. If all four leave you stuck, open the answer.

提示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.

这份答案在本机真跑过测试。但先确认你自己动手写过一遍 —— 读懂答案和写出答案是两种能力,考场上考的是后一种。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.