派生数据与状态提升:什么不该做成 stateValues you can compute, and lifting state up: what should not be state
isFormInvalid 为什么是一行普通变量,而不是第三个 useState。Why isFormInvalid is one plain variable and not a third useState.
这一页有什么On this page5
- 判断一个值该做成 state 还是当场算出来Decide whether a value should be state or should be computed on the spot
- 说清「多余 state」会带来什么问题Explain what problems extra state causes
- 解释为什么 notes 必须住在 NoteManager 而不是 NoteTableExplain why notes has to live in NoteManager and not in NoteTable
- 看懂按钮文字 Add/Update 是怎么来的See where the button text Add or Update comes from
第二个测试断言「输入为空时提交按钮 disabled」。它靠的是 isFormInvalid 这个派生值。把它做成 state 是新手常见的过度设计,还容易出现「和实际输入不同步」的 bug。The second test asserts that the submit button is disabled while the input is empty. That relies on the computed value isFormInvalid. Turning it into state is a common beginner habit, and it easily produces a value that no longer matches what is in the input.
react-notes-app/src/components/NoteForm/index.tsxisFormInvalid 与按钮文字isFormInvalid and the button text
react-notes-app/src/components/NoteForm/index.tsxreact-notes-app/src/components/NoteManager/index.tsx状态提升的落点Where the lifted state ends up
react-notes-app/src/components/NoteManager/index.tsx能算出来的,就不要存If you can compute it, do not store it
state 越少,能出错的地方越少。The less state you have, the fewer places there are for things to go wrong.
「表单是否无效」完全由 title 和 content决定。所以它不需要单独存 —— 每次渲染时算一遍就行:
这种「从已有 state 直接算出来的值」叫派生数据(derived state)。 它自动就是最新的,因为每次渲染都重算。
如果做成 useState 会怎样? 你得在每一处修改 title 或 content 的地方记得同步更新它。 漏掉一处,就出现「输入框有内容,按钮还是灰的」这种 bug。 这类 bug 的名字叫状态不一致, 根源就是「同一个事实存了两份」。
判断方法很简单:「这个值能不能只用现有的 state 和 props 算出来?」 能 → 别做 state。
“Is the form invalid” follows entirely from title and content. So it needs no storage of its own — work it out once per render:
A value computed straight out of state you already have is called derived state. It is always current, because every render recomputes it.
What happens if you make it a useState? Then every single place that changes title or content has to remember to update it too. Miss one and you get “the input has text but the button is still grey”. That family of bug is called inconsistent state, and the cause is always the same fact stored twice.
The test is simple:“can this value be worked out from the state and props I already have?” Yes → do not make it state.
react-notes-app/src/components/NoteForm/index.tsx按钮文字:同一个 prop 决定三件事The button text: one prop decides three things
noteToEdit 这一个 prop,同时决定了:
- 输入框里显示什么 —— 通过上一节那个 useEffect。
- 按钮上写 Add 还是 Update ——
{noteToEdit ? "Update" : "Add"}, 渲染时当场算。 - 提交时是新建还是更新 ——
id: noteToEdit ? noteToEdit.id : Date.now()。
第 4 个测试会显式断言按钮文字:expect(screen.getByTestId("form-submit-button")).toHaveTextContent("Update")。大小写和拼写必须一模一样 —— 写成 "update" 或 "Save" 都会挂。
This single noteToEdit prop decides three things at once:
- What the inputs show — through the useEffect from the last lesson.
- Whether the button says Add or Update —
{noteToEdit ? "Update" : "Add"}, worked out during render. - Whether submitting creates or updates —
id: noteToEdit ? noteToEdit.id : Date.now().
The fourth test asserts the button text outright: expect(screen.getByTestId("form-submit-button")).toHaveTextContent("Update"). Spelling and case have to match exactly —"update" or "Save" both fail.
react-notes-app/src/components/NoteForm/index.tsx状态提升:数据放在「需要它的组件的最近共同祖先」Lifting state up: put the data in the closest shared parent of the components that need it
notes 为什么必须住在 NoteManager? 因为有两方需要它:
NoteTable要读它来渲染。NoteForm的提交要改它 (通过 onSubmit 间接改)。
这两个组件是兄弟,而 React 的数据只能往下流。 所以数据必须放在它们的最近共同祖先 ——NoteManager。这个动作叫状态提升(lifting state up)。
反过来看 title / content: 只有 NoteForm 用得到,所以留在NoteForm 自己身上就好。不要无脑把所有 state 都提到顶层 —— 那会让顶层组件变成一个什么都管的怪物。
noteToEdit 是个有意思的中间情况: 它由 NoteTable 的点击产生(NoteItem → onEdit),被 NoteForm 消费。 所以它也必须住在 NoteManager。
Why does notes have to live in NoteManager? Because two parties need it:
NoteTablehas to read it to render.NoteForm’s submit has to change it (indirectly, through onSubmit).
Those two are siblings, and React data only flows downward. So the data has to sit at their closest common ancestor —NoteManager. The move has a name: lifting state up.
Now look at title / content the other way round: only NoteForm ever needs them, so they stay inside NoteForm. Do not hoist every piece of state to the top out of habit — that turns the top component into a monster that manages everything.
noteToEdit is an interesting middle case: it is produced by a click in NoteTable (NoteItem → onEdit) and consumed by NoteForm. So it has to live in NoteManager too.
动手做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.
假设要给 Notes Manager 加一个「显示当前有几条笔记」的文字。 这个数字应该怎么实现?
Say you have to add a line to Notes Manager that shows how many notes there are right now. How should that number be produced?
假设要加一个搜索框(在 NoteForm 上方,属于 NoteManager 的直接子元素), 输入关键词后表格只显示匹配的笔记。 搜索关键词这个 state 该放在哪?
Say you add a search box above NoteForm, as a direct child of NoteManager. Once a keyword is typed, the table shows only the notes that match. Where should the state holding that keyword go?
补出 isFormInvalid 和按钮的两处动态部分。 注意:只输入空格也应该算无效。
Fill in isFormInvalid and the two changing parts of the button. Note: spaces only should also count as invalid.
- isFormInvalid 是一个普通 const,不许用 useStateisFormInvalid is a plain const; useState is not allowed
- 只输入空格也要判定为无效(用 trim)Spaces only must also count as invalid (use trim)
- 按钮在表单无效时 disabledThe button is disabled while the form is invalid
- 按钮文字:noteToEdit 存在时是 Update,否则是 Add(大小写必须一致)Button text: Update when noteToEdit exists, otherwise Add (the capitals must match)
- 不许改动 data-testidDo not change any data-testid
看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。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.
换一道题也能用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.
- 能从现有 state / props 算出来的值,不要做成 state。If a value can be computed from existing state or props, do not make it state.
- 多余 state 的代价是「状态不一致」,而且是最难查的一类 bug。Extra state costs you consistency, and that is one of the hardest kinds of bug to find.
- isFormInvalid 是派生数据,每次渲染重算,永远和输入一致。isFormInvalid is computed. It is worked out again on every render, so it always matches the input.
- state 放在「需要它的组件的最近共同祖先」,不要一律提到顶层。Put state in the closest shared parent of the components that need it. Do not push everything to the top.
- 按钮文字 Add / Update 大小写必须一致 —— 测试会直接断言字符串。The button text Add and Update must match exactly, capital letters included, because the tests assert on the string.