DrillLab
第 06 / 21 节LESSON 06 / 21约 15 分钟~15 min

useEffect:把 props 的变化同步进 stateuseEffect: copying a change in props into state

Task 3 的「点 Edit 后内容回填到表单」,靠的就是这 9 行。In Task 3, clicking Edit puts the note back into the form. These 9 lines are what does it.

2 个练习2 exercisesReact · 第 2 部分React · Part 2
这一页有什么On this page6
学完这节你会After this lesson you can
  • 说清 useEffect 什么时候跑Explain when useEffect runs
  • 看懂依赖数组的三种写法各代表什么Read the three ways of writing the dependency array and what each one means
  • 解释 NoteForm 里那个 useEffect 为什么必须存在Explain why the useEffect in NoteForm has to be there
  • 知道 useEffect 无限循环是怎么造成的Know how a useEffect infinite loop happens
这在考试里考什么What the exam does with this

Task 3 要求「点 Edit → 内容回填进表单」。表单的 title/content 是 NoteForm 自己的 state,而触发源 noteToEdit 是外面传进来的 prop —— 把外部变化同步进内部 state,这正是 useEffect 的活。Task 3 asks that clicking Edit fills the form with the note. The title and content of the form are NoteForm's own state, while the trigger noteToEdit is a prop from outside. Copying an outside change into inside state is exactly the job of useEffect.

这节课要看的真实文件Real files this lesson looks at1 项 · 1 个可以展开看原文1 items · 1 can be opened
react-notes-app/src/components/NoteForm/index.tsx第 17–25 行那个 useEffectThe useEffect on lines 17 to 25
TSXindex.tsx源项目From source
1import React, { useState, useEffect } from "react";
2import type { Note } from "../../types/Note";
3
4interface NoteFormProps {
5 onSubmit: (note: Note) => void;
6 noteToEdit: Note | null;
7}
8
9const NoteForm: React.FC<NoteFormProps> = ({ onSubmit, noteToEdit }) => {
10 const [title, setTitle] = useState("");
11 const [content, setContent] = useState("");
12
13 useEffect(() => {
14 if (noteToEdit) {
15 setTitle(noteToEdit.title);
16 setContent(noteToEdit.content);
17 } else {
18 setTitle("");
19 setContent("");
20 }
21 }, [noteToEdit]);
22
23 const isFormInvalid = title.trim() === "" || content.trim() === "";
24
25 const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
26 event.preventDefault();
27 if (isFormInvalid) return;
28 const newNote = {
29 id: noteToEdit ? noteToEdit.id : Date.now(),
30 title: title.trim(),
31 content: content.trim(),
32 };
33 onSubmit(newNote);
34 setTitle("");
35 setContent("");
36 };
37
38 return (
39 <div className="card w-200 pt-30 pb-8 mt-15 mb-15">
40 <form onSubmit={handleSubmit} data-testid="note-form">
41 <section className="layout-row align-items-center justify-content-center mt-20 mr-20 ml-20">
42 <label className="form-title-label">Title:</label>
43 <input
44 type="text"
45 placeholder="Title"
46 value={title}
47 onChange={(e) => setTitle(e.target.value)}
48 data-testid="form-input"
49 className="form-input"
50 />
51 </section>
52
53 <section className="layout-row align-items-center justify-content-center mt-20 mr-20 ml-20">
54 <label className="form-content-label">Content:</label>
55 <textarea
56 placeholder="Content"
57 value={content}
58 onChange={(e) => setContent(e.target.value)}
59 data-testid="form-textarea"
60 className="form-textarea"
61 />
62 </section>
63
64 <section className="layout-row align-items-center justify-content-center mt-20 mr-20 ml-20">
65 <button
66 type="submit"
67 disabled={isFormInvalid}
68 data-testid="form-submit-button"
69 >
70 {noteToEdit ? "Update" : "Add"}
71 </button>
72 </section>
73 </form>
74 </div>
75 );
76};
77
78export default NoteForm;
Source: react-notes-app/src/components/NoteForm/index.tsx
§01

useEffect 在「渲染完成之后」跑useEffect runs after the render is finished

它不是渲染的一部分,是渲染的后续动作。It is not part of the render. It is what happens after the render.

useEffect(fn, deps) 的意思是:「这次渲染结束、DOM 更新完之后, 如果 deps 里有东西变了,就执行 fn」

依赖数组有三种写法,行为完全不同:

写法什么时候执行 fn典型用途
useEffect(fn, [])只在第一次渲染后执行一次初始化、拉一次数据
useEffect(fn, [a, b])第一次 + 之后每次 a 或 b 变化同步:外部变了,内部跟上
useEffect(fn)每一次渲染后都执行几乎总是写错了

第三种是无限循环的常见来源:fn 里改了 state → 触发重渲染 → fn 又执行 → 又改 state → …… React 最后会抛Maximum update depth exceeded

useEffect(fn, deps) means:“once this render is finished and the DOM is updated, if anything in deps changed, run fn”.

There are three ways to write the dependency array, and they behave completely differently:

FormWhen fn runsTypical use
useEffect(fn, [])Once, after the first renderSetup, one-off fetch
useEffect(fn, [a, b])First render, then whenever a or b changesSyncing: something outside moved, catch up
useEffect(fn)After every renderAlmost always a mistake

The third one is the usual source of infinite loops: fn changes state → re-render → fn runs again → changes state again → ... and React eventually throws Maximum update depth exceeded.

§02

读懂项目里这个 useEffectRead the useEffect in this project

9 行代码,把 Task 3 的一半工作做完了。Nine lines of code do half of Task 3.

问题是这样的:NoteForm 的输入框内容存在它自己的 state(title / content)里。而「用户点了哪条笔记的 Edit」 这件事发生在外面, 通过 noteToEdit 这个 prop 传进来。

所以需要一条规则:「每当 noteToEdit 变了, 就把内部两个 state 改成它的值」。 这正是 useEffect(fn, [noteToEdit])

else 分支也重要:noteToEdit变回 null(编辑完成、退出编辑模式)时, 要清空表单。没有这个 else, 提交完之后表单里还留着刚才编辑的内容。

为什么不能直接写在渲染里?因为在组件函数体里直接调 setTitle会立刻触发新一轮渲染,而那一轮又会再调一次 —— 死循环。useEffect + 依赖数组保证了 「只在 noteToEdit 真的变了的时候才动手」。

Here is the problem. The text in NoteForm’s inputs lives in its own state (title / content). But “which note’s Edit did the user press” happens outside, and arrives through the noteToEdit prop.

So you need one rule: “whenever noteToEdit changes, set the two inner pieces of state to its values”. That is precisely useEffect(fn, [noteToEdit]).

The else branch matters too: when noteToEdit goes back to null (the edit is done, edit mode is over), the form has to be cleared. Without that else, the text you were just editing is still sitting there after you submit.

Why not put it straight in the render?Because calling setTitle in the component body schedules another render immediately, and that render calls it again — a dead loop. useEffect plus a dependency array guarantees you only act when noteToEdit really changed.

TSXsrc/components/NoteForm/index.tsx(第 17–25 行)src/components/NoteForm/index.tsx (lines 17–25)源项目From source
1useEffect(() => {
2 if (noteToEdit) {
3 setTitle(noteToEdit.title);
4 setContent(noteToEdit.content);
5 } else {
6 setTitle("");
7 setContent("");
8 }
9}, [noteToEdit]);
Source: react-notes-app/src/components/NoteForm/index.tsx
第 9 行那个 [noteToEdit] 是整段的开关。改成 [] 就只在首次渲染跑一次,点 Edit 永远不回填;去掉它就变成每次渲染都跑,直接死循环。The [noteToEdit] on line 9 is the switch for the whole block. Change it to [] and the effect runs only on the first render, so Edit never prefills. Remove it and the effect runs on every render, which is an endless loop.
§03

依赖数组写错的三种后果Three things that go wrong when the dependency array is wrong

拿这段代码做实验,三种写法对应三种病:

  1. [] —— 只在首次渲染跑一次。点 Edit 时 noteToEdit变了,但 effect 不再执行。症状:点 Edit 按钮文字变成了 Update, 但输入框是空的。(因为按钮文字是渲染时直接读 prop 算的,不依赖 effect。)
  2. 什么都不写 —— 每次渲染后都跑。 effect 里调了 setTitle → 重渲染 → 又跑 → ……症状:Maximum update depth exceeded,页面卡死。
  3. [noteToEdit, title, content] —— 把自己改的 state 也放进依赖。 setTitle 改了 title → title 变了 → effect 又跑 → setTitle 又执行 → ……症状:同样死循环。
    这条特别值得记:effect 里改的 state, 不要放进它自己的依赖数组。

Take that code and try three versions of the array. Each one gets you a different illness:

  1. [] — runs once after the first render. noteToEdit changes when you press Edit, but the effect never runs again. Symptom: pressing Edit turns the button into Update, but the inputs stay empty. (The button text is computed from the prop during render, so it does not need the effect.)
  2. Nothing at all — runs after every render. The effect calls setTitle → re-render → runs again → ... Symptom: Maximum update depth exceeded, and the page freezes.
  3. [noteToEdit, title, content] — the state the effect itself writes is in its own dependencies. setTitle changes title → title changed → the effect runs again → setTitle again → ... Symptom: the same dead loop.
    This one is worth committing to memory: state that an effect writes does not belong in that effect’s dependency array.
练习Practice

动手做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.

L2填空Fill the blanks补全编辑回填的 useEffectComplete the useEffect that prefills the form for editing

这是 NoteForm 里那个决定 Task 3 成败的 effect。 两个空:一个分支条件,一个依赖数组。

This is the effect in NoteForm that decides whether Task 3 passes. Two blanks: one branch condition, one dependency array.

TSXsrc/components/NoteForm/index.tsx2 个空2 blanks
1useEffect(() => {
2 if () {
3 setTitle(noteToEdit.title);
4 setContent(noteToEdit.content);
5 } else {
6 setTitle("");
7 setContent("");
8 }
9}, );
把 2 个空都填上才能检查(还差 2 个)Fill all 2 blanks to check (2 to go)
L3Debug LabDebug LabDebug Lab · 点 Edit 之后页面卡死Debug Lab · the page freezes after you press Edit

点某一行的 Edit 按钮,浏览器标签页转圈,控制台刷出大量警告。 请判断类型并定位。

Press the Edit button on any row. The browser tab spins and the console fills up with warnings. Classify the error, then locate it.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render. at NoteForm (src/components/NoteForm/index.tsx:13:3) at NoteManager (src/components/NoteManager/index.tsx:6:3) Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
TSX有问题的依赖数组The broken dependency array示意Illustrative
1useEffect(() => {
2 if (noteToEdit) {
3 setTitle(noteToEdit.title);
4 setContent(noteToEdit.content);
5 } else {
6 setTitle("");
7 setContent("");
8 }
9}, [noteToEdit, title, content]);
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
错例Wrong

初学者常见的几种写法错误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.

TSX示意Illustrative
1// ✗ 直接在组件函数体里同步 —— 立刻死循环
2const NoteForm = ({ noteToEdit }) => {
3 const [title, setTitle] = useState("");
4 if (noteToEdit) setTitle(noteToEdit.title); // 渲染期间调 setState
5 ...
6};
1// ✗ syncing straight inside the component body — an endless loop
2const NoteForm = ({ noteToEdit }) => {
3 const [title, setTitle] = useState("");
4 if (noteToEdit) setTitle(noteToEdit.title); // setState during render
5 ...
6};
在渲染期间调用 setter 会立刻安排下一次渲染, 而下一次渲染又会再调一次。「渲染要纯粹」是 React 的基本规则: 组件函数只负责根据当前 props/state 算出 JSX, 不做任何副作用。副作用放 useEffectCalling a setter during render immediately schedules another render, and that render calls the setter again.Rendering has to be pure is a basic React rule: the component function only turns the current props and state into JSX, and does nothing else. Anything else belongs in useEffect.
TSX示意Illustrative
1// ✗ 依赖写成 [] —— 点 Edit 时不回填
2useEffect(() => {
3 if (noteToEdit) { setTitle(noteToEdit.title); ... }
4}, []);
1// ✗ dependencies written as [] — Edit does not prefill
2useEffect(() => {
3 if (noteToEdit) { setTitle(noteToEdit.title); ... }
4}, []);
[] = 只在首次渲染后跑一次。首次渲染时noteToEditnull, 所以什么都没发生;后面点 Edit,effect 不再执行。
这个 bug 特别迷惑人:按钮文字会正确变成 "Update"(那是渲染时直接读 prop 算的), 让人以为「Edit 生效了」,但输入框是空的。
[] means the effect runs once, after the first render. On that first render noteToEdit is null, so nothing happens, and when you click Edit later the effect never runs again.
This bug is confusing: the button text does change to "Update", because that is computed straight from the prop during render. It looks like Edit worked, but the inputs are empty.
迁移Transfer

换一道题也能用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.

「外部数据变了,内部状态要跟上」Outside data changed and inside state has to follow
useEffect(fn, [那个外部数据])useEffect(fn, [that outside value])
「组件加载时做一次某事」Do something once, when the component first appears
useEffect(fn, [])useEffect(fn, [])
Maximum update depth exceededMaximum update depth exceeded
查依赖数组:是不是漏了,或含了自己改的 stateCheck the dependency array: it is missing, or it holds the state this effect changes
「点了却没反应,但别的地方变了」Clicking does nothing here, but something else did change
查依赖数组是不是写成了 []Check whether the dependency array was written as []
这节的要点What to take away
  1. useEffect 在渲染完成后跑,依赖数组决定它跑不跑。useEffect runs after the render is finished, and the dependency array decides whether it runs at all.
  2. [] 只跑一次;[a] 在 a 变化时跑;不写则每次渲染都跑(通常是错的)。[] runs once. [a] runs when a changes. No array at all runs after every render, which is usually wrong.
  3. NoteForm 那个 effect 的职责是「把外部的 noteToEdit 同步进内部两个 state」。The job of that effect in NoteForm is to copy the outside noteToEdit into its two inside state values.
  4. else 分支负责在退出编辑时清空表单,不能省。The else branch clears the form when editing ends. Do not leave it out.
  5. effect 里改的 state 不能放进它自己的依赖数组,否则死循环。State that the effect itself changes must not be in that effect's dependency array, or it loops forever.

接下来What next

  1. 把这一节的练习做掉Do this lesson’s exercises2 个,就在这一页上面 —— 别攒着最后一起做2 of them, further up this page — do not save them for later
    回到练习 ↑Back up to them ↑
  2. 接着看下一节Continue to the next lesson派生数据与状态提升:什么不该做成 stateValues you can compute, and lifting state up: what should not be state
    下一节Next lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: 列表渲染与 keyRendering a list, and the key prop