DrillLab
第 03 / 21 节LESSON 03 / 21约 14 分钟~14 min

useState:让界面跟着数据变useState: making the screen follow the data

两个 state 撑起了整道 Q1:notes 和 noteToEdit。Two pieces of state carry the whole of Q1: notes and noteToEdit.

2 个练习2 exercisesReact · 第 1 部分React · Part 1
这一页有什么On this page7
学完这节你会After this lesson you can
  • 说清 useState 返回的两个东西各是什么Explain what each of the two things useState returns is
  • 知道为什么必须用 setter 而不能直接赋值Know why you have to use the setter instead of assigning a new value
  • 会用函数式更新 setX(prev => ...) 并说清它比 setX(newValue) 好在哪Use the updater form setX(prev => ...) and say what makes it safer than setX(newValue)
  • 看懂一次点击是怎么最终变成新界面的Follow how one click turns into a new screen
这在考试里考什么What the exam does with this

Q1 的判卷标准就是「点了按钮之后界面对不对」。state 用错,四个测试全挂。这是整门考试最核心的一节。Q1 is graded on one thing: is the screen correct after the button is clicked. Use state wrong and all four tests fail. This is the most important lesson in the whole exam.

这节课要看的真实文件Real files this lesson looks at1 项 · 1 个可以展开看原文1 items · 1 can be opened
react-notes-app/src/components/NoteManager/index.tsx两个 state 与三个 handler 的全部真实代码The full real code for the two pieces of state and the three handlers
TSXindex.tsx源项目From source
1import { useState } from "react";
2import type { Note } from "../../types/Note";
3import NoteForm from "../NoteForm";
4import NoteTable from "../NoteTable";
5
6const NoteManager: React.FC = () => {
7 const [notes, setNotes] = useState<Note[]>([]);
8 const [noteToEdit, setNoteToEdit] = useState<Note | null>(null);
9
10 const handleSubmitNote = (submittedNote: Note) => {
11 if (noteToEdit) {
12 setNotes((prev) =>
13 prev.map((note) =>
14 note.id === submittedNote.id ? submittedNote : note,
15 ),
16 );
17 setNoteToEdit(null);
18 } else {
19 setNotes((prev) => [...prev, submittedNote]);
20 }
21 };
22
23 const handleDelete = (id: number) => {
24 setNotes((prev) => prev.filter((note) => note.id !== id));
25 };
26
27 const handleEdit = (note: Note) => {
28 setNoteToEdit(note);
29 };
30
31 return (
32 <div
33 className="layout-column align-items-center justify-content-start"
34 data-testid="note-manager"
35 >
36 <NoteForm onSubmit={handleSubmitNote} noteToEdit={noteToEdit} />
37 <NoteTable notes={notes} onDelete={handleDelete} onEdit={handleEdit} />
38 </div>
39 );
40};
41
42export default NoteManager;
Source: react-notes-app/src/components/NoteManager/index.tsx
§01

普通变量为什么不行Why a plain variable does not work

组件函数每次渲染都会重新执行一遍。普通变量活不过这一遍。The component function runs again on every render. A plain variable does not survive that.

这是理解 React 最关键的一件事:组件函数会被反复调用。 每次界面需要更新,React 就把你的组件函数再执行一次, 拿到新的 JSX,然后对比、更新真实 DOM。

所以如果你在组件里写 let notes = [], 那么每次重新渲染,这行都会重新执行,notes 又变回空数组。数据存不住。

useState 解决的正是这个问题: 它让 React 在组件外部替你记住这个值, 每次重新渲染时把上次的值交还给你。

This is the most important thing to understand about React: your component function gets called again and again. Every time the UI needs to change, React runs your function once more, takes the new JSX, compares it, and updates the real DOM.

So if you write let notes = [] inside the component, that line runs again on every render and notes is back to an empty array. The data cannot survive.

useState exists for exactly this problem: it asks React to remember the value outside your component and hand you last render’s value each time it runs.

TSX示意Illustrative
1// ✗ 普通变量:每次渲染都被重置
2const NoteManager = () => {
3 let notes: Note[] = []; // 每次渲染都是空数组
4 const add = (n: Note) => { notes.push(n); }; // 加进去了,但下次渲染就没了
5 ...
6};
7
8// ✓ useState:React 帮你记住
9const NoteManager = () => {
10 const [notes, setNotes] = useState<Note[]>([]); // 初始值只在第一次生效
11 ...
12};
1// ✗ A plain variable: reset on every render
2const NoteManager = () => {
3 let notes: Note[] = []; // an empty array on every render
4 const add = (n: Note) => { notes.push(n); }; // it goes in, and the next render loses it
5 ...
6};
7
8// ✓ useState: React remembers it for you
9const NoteManager = () => {
10 const [notes, setNotes] = useState<Note[]>([]); // the initial value only counts the first time
11 ...
12};
§02

useState 返回一个数组,里面两样东西useState returns an array with two things in it

const [notes, setNotes] = useState<Note[]>([])这一行做了三件事:

  1. 声明一块由 React 保管的状态,初始值 []这个初始值只在第一次渲染时用, 之后的渲染会忽略它。
  2. notes 拿到当前这次渲染看到的值。
  3. setNotes唯一合法的修改途径。 调用它 = 告诉 React「值变了,请重新渲染」。

useState 返回的是数组, 所以用 [a, b] 这种数组解构, 名字随你起(但习惯上是 x / setX)。

真实项目里的两个 state:

const [notes, setNotes] = useState<Note[]>([]) does three things in one line:

  1. Declares a slot of state that React looks after, starting at []. That initial value is only used on the first render; every render after that ignores it.
  2. notes holds the value this particular render sees.
  3. setNotes is the only legal way to change it. Calling it means “the value changed, please render again”.

useState returns an array, which is why you take it apart with [a, b]. The names are yours to pick (though x / setX is the convention).

The two pieces of state in the real project:

TSXsrc/components/NoteManager/index.tsx(开头)src/components/NoteManager/index.tsx (the opening)源项目From source
1const NoteManager: React.FC = () => {
2 const [notes, setNotes] = useState<Note[]>([]);
3 const [noteToEdit, setNoteToEdit] = useState<Note | null>(null);
Source: react-notes-app/src/components/NoteManager/index.tsx
noteToEdit 用 null 表示「现在不在编辑任何东西」。这个 state 是 Task 3 的核心 —— 它同时决定了「表单里显示什么」和「按钮上写 Add 还是 Update」。A null value for noteToEdit means nothing is being edited right now. This piece of state is the center of Task 3: it decides both what the form shows and whether the button reads Add or Update.
§03

为什么用 setNotes(prev => ...) 而不是 setNotes([...notes, n])Why setNotes(prev => ...) and not setNotes([...notes, n])

两种都能用。但前者在一种情况下明显更安全。Both forms work. But the first one is clearly safer in one situation.

notes 这个变量拿到的是当前这次渲染时的快照。 如果你在同一个事件里连续调用两次 setter, 第二次看到的 notes 还是旧的:

函数式更新 setNotes(prev => ...)里的 prev 是 React 交给你的 「此刻最新的值」,连续调用也不会丢。

Q1 里其实不会连续调两次,所以两种写法都能过测试。 但真实项目里的代码统一用了函数式更新 ——这是更稳的默认习惯,跟着写就对了

The notes variable holds a snapshot from this render. If you call the setter twice inside the same event, the second call still sees the old notes:

With a functional update setNotes(prev => ...) — the prev React hands you is “the freshest value right now”, so back-to-back calls lose nothing.

Q1 never actually calls the setter twice in a row, so both styles pass the tests. But the real project uses functional updates everywhere —it is the steadier default, so just write it that way.

TSX示意Illustrative
1// 假设想一次加两条
2setNotes([...notes, a]); // notes 是旧的 → 结果 [a]
3setNotes([...notes, b]); // notes 还是旧的 → 结果 [b],a 丢了
4
5// 函数式更新
6setNotes((prev) => [...prev, a]); // prev = [] → [a]
7setNotes((prev) => [...prev, b]); // prev = [a] → [a, b] ✓
1// Say you want to add two notes at once
2setNotes([...notes, a]); // notes is the old value → result [a]
3setNotes([...notes, b]); // notes is still old → result [b], a is gone
4
5// Functional update
6setNotes((prev) => [...prev, a]); // prev = [] → [a]
7setNotes((prev) => [...prev, b]); // prev = [a] → [a, b] ✓
§04

一次点击的完整旅程The full path of one click

把这条链走通,你就真的懂 React 了。Once you can follow this chain end to end, you really do understand React.

下面是「点 Delete 按钮」这一下,从手指到屏幕之间发生的全部事情。 一步一步点过去。

点一下 Delete,屏幕上少一行 —— 中间这五步第 1 / 5 步Step 1 of 5
用户
点击 Delete
鼠标点在 id=2 那一行
子组件
NoteItem
父组件
NoteManager
notes = [1, 2, 3]
React
重新渲染
浏览器
真实 DOM
3 个 <tr>
起点:用户点了第二行的 Delete。此刻 notes 里还是三条。

关键在第 4 步:React 并不知道「哪一行被删了」。 它只知道「state 变了」,于是把 NoteManager整个重新执行一遍,拿到新的 JSX,再和上一次的对比, 最后只把真正变化的 DOM 改掉。

所以你不需要手动去删 DOM 节点、 不需要 document.querySelector。 你只管改数据,界面自己跟上。 这就是 React 的全部承诺。

Below is everything that happens between your finger and the screen when you press Delete. Step through it one frame at a time.

点一下 Delete,屏幕上少一行 —— 中间这五步第 1 / 5 步Step 1 of 5
用户
点击 Delete
鼠标点在 id=2 那一行
子组件
NoteItem
父组件
NoteManager
notes = [1, 2, 3]
React
重新渲染
浏览器
真实 DOM
3 个 <tr>
起点:用户点了第二行的 Delete。此刻 notes 里还是三条。

Step 4 is the one that matters: React has no idea which row was deleted. All it knows is that state changed, so it runs NoteManager again from the top, takes the new JSX, compares it with the previous one, and changes only the DOM that really differs.

Which means you never remove DOM nodes by hand and never need document.querySelector. You change the data, the UI keeps up. That is the whole promise of React.

练习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.

L1排顺序Order it把一次点击的顺序排对Put the steps of one click in order

用户点了某一行的 Delete 按钮。把下面五件事按发生顺序排好。

The user clicked the Delete button on one row. Put the five things below in the order they happen.

1React 重新执行 NoteManager 函数,拿到新的 JSXReact runs the NoteManager function again and gets new JSX
2NoteItem 的 onClick 触发,调用 onDelete(note.id)The onClick of NoteItem fires and calls onDelete(note.id)
3React 对比新旧 JSX,把变化的部分写进真实 DOMReact compares the new JSX with the old one and writes only the differences into the real DOM
4NoteManager 里的 handleDelete 执行,调用 setNotes(...)handleDelete inside NoteManager runs and calls setNotes(...)
5React 记下 notes 的新值,标记这个组件需要重新渲染React records the new value of notes and marks this component for a re-render
L3写整块Write a block自己写出 NoteManager 的两个 state 和删除逻辑Write the two states of NoteManager and the delete logic yourself

只给你组件外壳。按要求补出两个 state 和 handleDelete。 不要看下面的答案,先自己写。

You get only the shell of the component. Add the two states and handleDelete as described. Write it yourself before you look at the answer below.

要求Requirements
  • 用 useState 声明 notes,类型是 Note[],初始值为空数组Declare notes with useState, typed Note[], starting as an empty array
  • 用 useState 声明 noteToEdit,类型是 Note | null,初始值为 nullDeclare noteToEdit with useState, typed Note | null, starting as null
  • handleDelete 按 id 移除对应笔记,必须用函数式更新,不许改动原数组handleDelete removes the matching note by id, using a functional update, without changing the original array
TSXsrc/components/NoteManager/index.tsx
This check is textual: it looks for the right constructs, it does not run your code
提示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.

看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。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.

错例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// ✗ 直接赋值 —— React 完全不知道发生了什么
2notes = [...notes, newNote];
1// ✗ Assigning straight to the variable — React never learns anything happened
2notes = [...notes, newNote];
notesconst 声明的, 这行连编译都过不去。就算改成 let, React 也不会知道值变了 —— 它只监听 setter 的调用。唯一的修改途径是 setNotes。notes is declared with const, so this line does not even compile. Even as a let, React would not know the value changed — it only watches for setter calls. setNotes is the only way to change it.
TSX示意Illustrative
1// ✗ 以为 setState 是同步的
2setNotes((prev) => [...prev, newNote]);
3console.log(notes.length); // 还是旧的长度!
1// ✗ Assuming setState is synchronous
2setNotes((prev) => [...prev, newNote]);
3console.log(notes.length); // still the old length!
setNotes 只是「预约一次重新渲染」, 它不会当场改变 notes 这个变量。 新值要到下一次渲染才看得到。 想在更新后做点什么,用 useEffect(下一节讲)。setNotes only asks React for one more render. It does not change the notes variable on the spot. The new value is visible on the next render. To do something after the update, use useEffect (the next lesson).
迁移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.

「界面要跟着某个数据变」The screen has to follow some piece of data
把它做成 useStatePut that data in useState
初始值是 [] 或 nullThe initial value is [] or null
显式写泛型参数Write the generic type argument explicitly
「基于当前值算出新值」The new value is computed from the current one
setX(prev => ...)setX(prev => ...)
setState 之后 console.log 是旧值console.log after setState shows the old value
正常,新值在下次渲染才有That is expected. The new value arrives on the next render
这节的要点What to take away
  1. 组件函数会被反复执行,所以普通变量存不住数据 —— 这是 useState 存在的原因。The component function runs again and again, so a plain variable cannot hold data. That is the reason useState exists.
  2. useState 返回 [当前值, setter];初始值只在第一次渲染生效。useState returns [current value, setter]. The initial value is used only on the first render.
  3. setter 是唯一合法的修改途径,调用它等于「预约一次重新渲染」。The setter is the only legal way to change the value. Calling it asks React for one more render.
  4. setX(prev => ...) 比 setX(新值) 稳,项目里统一用前者。setX(prev => ...) is safer than setX(newValue), and this project uses the first form everywhere.
  5. 你只管改数据,DOM 由 React 对比后自动更新 —— 不要自己操作 DOM。You change the data, and React compares and updates the DOM for you. Do not touch the DOM yourself.

接下来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受控输入:value + onChange 的闭环Controlled inputs: the loop between value and onChange
    下一节Next lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: props:数据往下流,事件往上报props: data flows down, events go back up