受控输入:value + onChange 的闭环Controlled inputs: the loop between value and onChange
输入框里的字,其实存在 React 的 state 里,不在 DOM 里。The text you type sits in React state, not in the DOM.
这一页有什么On this page7
- 01 「受控」的意思是:唯一真相在 state 里Controlled means the only source of truth is the state
- 02 只写 value 不写 onChange 会怎样What happens if you write value but no onChange
- 03 表单提交:preventDefault 不是可选项Submitting a form: preventDefault is not optional
- 04 把整个 NoteForm 读一遍Read the whole of NoteForm once
- 练习 · 动手做Practice
- 常见错误Common mistakes
- 迁移模式Transfer
- 说清「受控」到底控的是什么Explain what a controlled input actually controls
- 写出 value + onChange 的完整闭环Write the full value + onChange loop
- 知道只写 value 不写 onChange 会怎样Know what happens if you write value but no onChange
- 看懂表单提交里 event.preventDefault() 的必要性See why event.preventDefault() is needed when a form is submitted
判卷测试用 userEvent.type() 往输入框里打字,然后断言表格内容。如果输入框不是受控的,打进去的字拿不到,Task 1 直接挂。The grading tests type into the input with userEvent.type() and then assert on the table contents. If the input is not controlled, the typed text never reaches your code and Task 1 fails.
react-notes-app/src/components/NoteForm/index.tsx两个受控输入 + 表单提交的完整实现The complete implementation of the two controlled inputs and the form submit
react-notes-app/src/components/NoteForm/index.tsx「受控」的意思是:唯一真相在 state 里Controlled means the only source of truth is the state
输入框自己不做主,它只显示 state 告诉它的东西。The input decides nothing on its own. It shows whatever the state tells it to show.
原生 HTML 里,<input> 自己记着用户输入了什么。 你要读它得 document.querySelector(...).value。 这叫非受控。
React 里的常规做法是反过来:
value={title}—— 输入框显示什么, 由 state 决定。onChange={(e) => setTitle(e.target.value)}—— 用户敲键盘时,把新值写进 state。- state 变了 → 重新渲染 → 输入框显示新值。
看起来绕了一圈,但换来一个巨大好处:任何时候,title 这个变量就是输入框里的内容。你不需要去 DOM 里读,也不会出现「显示的和读到的不一致」。
这个闭环叫 受控组件(controlled component)。
In plain HTML an <input> remembers what the user typed. To read it you go through document.querySelector(...).value. That is uncontrolled.
React normally turns it around:
value={title}— what the input shows is decided by state.onChange={(e) => setTitle(e.target.value)}— as the user types, the new value goes into state.- state changed → re-render → the input shows the new value.
It looks like a detour, and it buys one big thing: at any moment, the title variable is exactly what is inside the input. You never read the DOM, so “what is shown” can never drift from “what you read”.
This loop is called a controlled component.
react-notes-app/src/components/NoteForm/index.tsx只写 value 不写 onChange 会怎样What happens if you write value but no onChange
输入框会变成只读的。这是个很容易踩的坑。The input turns read-only. This mistake is very easy to make.
想清楚这个逻辑链:value={title} 意味着 「输入框显示的永远是 title」。 而 title 只能通过 setTitle 改。 如果没有 onChange,就没人调 setTitle,title 永远是初始值 ""。
结果:你在输入框里敲什么都不显示。React 开发模式下会给一条警告:
这条警告也提示了另一条路:如果你只是想给一个初始值、 之后不管它,用 defaultValue(非受控)。 但这道题必须用受控 —— 因为提交时要读到内容, 而且 Task 3 要能从外部把值填进去。
Follow the chain. value={title} means “this input always shows title”. And title can only change through setTitle. With no onChange, nobody ever calls setTitle, so title stays at its initial "".
Result: nothing you type shows up. React gives you a warning in development mode:
That warning also points at the other road: if you only want to set an initial value and never touch it again, use defaultValue (uncontrolled). This paper needs controlled inputs though — submitting has to read the content, and Task 3 has to push a value in from outside.
表单提交:preventDefault 不是可选项Submitting a form: preventDefault is not optional
这个项目用的是真正的 <form> +<button type="submit">, 提交处理挂在 form 的 onSubmit 上。
浏览器的默认行为是:提交表单 = 发一个 HTTP 请求并刷新页面。在单页应用里这是灾难 —— 页面一刷新,所有 state 归零, 刚添加的笔记全没了。
event.preventDefault() 就是在说 「别做默认那件事,我自己处理」。忘了它, Task 1 的表现是「点 Add 之后页面闪一下,什么都没发生」。
看真实实现里 handleSubmit 的五个动作,顺序很讲究:
event.preventDefault()—— 先拦住浏览器。if (isFormInvalid) return—— 空内容就不提交。- 构造 note。注意 id:
noteToEdit ? noteToEdit.id : Date.now()——这一行同时服务 Add 和 Update 两种情况, 是 Task 3 的关键之一。 onSubmit(newNote)—— 上报给父组件。setTitle("")/setContent("")—— 清空表单。
This project uses a real <form> plus<button type="submit">, with the submit handler sitting on the form’s onSubmit.
The browser’s default behaviour is: submitting a form means firing an HTTP request and reloading the page. In a single-page app that is a disaster — one reload and every piece of state is back to zero, including the note you just added.
event.preventDefault() is how you say “skip the default, I will handle this myself”. Forget it and Task 1 looks like this: you press Add, the page blinks, nothing happens.
Here are the five things the real handleSubmit does, and the order is deliberate:
event.preventDefault()— hold the browser back first.if (isFormInvalid) return— do not submit empty content.- Build the note. Watch the id:
noteToEdit ? noteToEdit.id : Date.now()—that one line serves both Add and Update, and it is one of the keys to Task 3. onSubmit(newNote)— report it up to the parent.setTitle("")/setContent("")— clear the form.
react-notes-app/src/components/NoteForm/index.tsx把整个 NoteForm 读一遍Read the whole of NoteForm once
这是这道题最密集的一个文件。上面讲的四件事都在里面。This is the densest file in the task. All four points above appear in it.
读的时候留意四处:两个 state(第 10–11 行)、useEffect 同步(13–21 行,下一节细讲)、派生数据 isFormInvalid(23 行)、按钮文字随 noteToEdit 变(70 行)。
Four spots to watch as you read: the two pieces of state (lines 10–11), the useEffect that syncs (lines 13–21, covered in the next lesson), the derived isFormInvalid (line 23), and the button text following noteToEdit (line 70).
react-notes-app/src/components/NoteForm/index.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.
把 NoteForm 里 textarea 那一段补全。 三个空构成一个完整的闭环。
Fill in the textarea part of NoteForm. The three blanks together form one complete loop.
填好标题和内容,点 Add。页面明显闪了一下, 地址栏出现了 ?,表格还是空的。
Fill in a title and some content, then press Add. The page clearly blinks, a ? appears in the address bar, and the table is still empty.
初学者常见的几种写法错误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.
e.target 是那个 DOM 元素对象,不是字符串。 TypeScript 会报Argument of type 'EventTarget' is not assignable to parameter of type 'string'。 这是好事 —— 在 JavaScript 项目里,这个错会一直藏到运行时。e.target is the DOM element object, not a string. TypeScript reportsArgument of type 'EventTarget' is not assignable to parameter of type 'string'. That is a good thing. In a plain JavaScript project this mistake stays hidden until the code runs.clear 再type,所以不清空不一定让测试挂。 但用户体验上,添加完一条之后输入框还留着上一条的内容, 明显是 bug。题目没写的细节,也是评分点。The fourth test (editing) calls clear before it callstype, so leaving the form filled doesnot always fail the tests. But for the user, an input that still holds the previous note after it was added is clearly a bug.Details the task did not spell out are graded too.换一道题也能用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 决定(value),输入写回 state(onChange),形成闭环。A controlled input shows what the state says (value) and writes typing back into the state (onChange). That closes the loop.
- e.target.value 才是内容;e.target 是元素,e 是事件。e.target.value is the text. e.target is the element, and e is the event.
- 只写 value 不写 onChange,输入框会变成只读。value without onChange makes the input read-only.
- form 提交必须 event.preventDefault(),否则页面刷新、state 归零。A form submit needs event.preventDefault(), or the page reloads and all state is reset.
- id: noteToEdit ? noteToEdit.id : Date.now() 一行同时服务新增和更新。The single line id: noteToEdit ? noteToEdit.id : Date.now() serves adding and updating at the same time.