DrillLab
第 02 / 21 节LESSON 02 / 21约 12 分钟~12 min

props:数据往下流,事件往上报props: data flows down, events go back up

为什么 NoteItem 里的 Delete 按钮,最终改的是 NoteManager 里的数据。Why the Delete button inside NoteItem ends up changing data that lives in NoteManager.

2 个练习2 exercisesReact · 第 1 部分React · Part 1
这一页有什么On this page6
学完这节你会After this lesson you can
  • 说清 props 是什么、方向是什么Explain what props are and which direction they travel
  • 看懂「把函数当 props 传下去」这个模式Read the pattern of passing a function down as props
  • 分清 onClick={fn} 和 onClick={fn()} 的区别Tell onClick={fn} apart from onClick={fn()}
  • 知道为什么子组件不能直接改父组件的数据Know why a child component cannot change the parent's data directly
这在考试里考什么What the exam does with this

Q1 的三个任务全都是「子组件报告事件 → 父组件改 state」。props 传函数这个模式如果没想通,Delete 和 Edit 两题都会卡住。All three Q1 tasks have the same shape: the child reports an event, then the parent changes state. If passing a function through props is not clear to you, both the Delete task and the Edit task will stop you.

这节课要看的真实文件Real files this lesson looks at2 项 · 2 个可以展开看原文2 items · 2 can be opened
react-notes-app/src/components/NoteTable/index.tsx把 props 原样往下传Passes props straight down
TSXindex.tsx源项目From source
1import React from "react";
2import type { Note } from "../../types/Note";
3import NoteItem from "../NoteItem";
4
5export interface NoteTableProps {
6 notes: Note[];
7 onDelete: (id: number) => void;
8 onEdit: (note: Note) => void;
9}
10
11const NoteTable: React.FC<NoteTableProps> = ({ notes, onDelete, onEdit }) => {
12 return (
13 <div className="card w-30 pt-30 pb-8 mt-2">
14 <table>
15 <thead>
16 <tr>
17 <th>Title</th>
18 <th>Content</th>
19 <th>Edit</th>
20 <th>Delete</th>
21 </tr>
22 </thead>
23 <tbody data-testid="notes-list">
24 {notes.map((note) => (
25 <NoteItem
26 key={note.id}
27 note={note}
28 onDelete={onDelete}
29 onEdit={onEdit}
30 />
31 ))}
32 </tbody>
33 </table>
34 </div>
35 );
36};
37
38export default NoteTable;
Source: react-notes-app/src/components/NoteTable/index.tsx
react-notes-app/src/components/NoteItem/index.tsx调用 props 里的函数上报Reports upwards by calling a function from props
TSXindex.tsx源项目From source
1import React from "react";
2import type { Note } from "../../types/Note";
3
4export interface NoteItemProps {
5 note: Note;
6 onDelete: (id: number) => void;
7 onEdit: (note: Note) => void;
8}
9
10const NoteItem: React.FC<NoteItemProps> = ({ note, onDelete, onEdit }) => {
11 return (
12 <tr>
13 <td>{note.title}</td>
14 <td>{note.content}</td>
15 <td>
16 <button onClick={() => onEdit(note)} className="outlined">
17 Edit
18 </button>
19 </td>
20 <td>
21 <button onClick={() => onDelete(note.id)} className="danger">
22 Delete
23 </button>
24 </td>
25 </tr>
26 );
27};
28
29export default NoteItem;
Source: react-notes-app/src/components/NoteItem/index.tsx
§01

props 就是函数参数props are just function arguments

组件是函数,props 是传给它的那个对象。A component is a function, and props is the object you pass to it.

<NoteItem note={n} onDelete={handleDelete} />, 等于调用 NoteItem({ note: n, onDelete: handleDelete })props 没有任何魔法,就是一个普通对象。

所以组件里那个 ({ note, onDelete, onEdit })是在解构这个对象 —— 上一门课讲过的解构语法。

props 是只读的。子组件不许改它:note.title = "新标题" 这种写法 在 React 里是禁止的(TypeScript 不一定拦得住你, 但界面不会更新,而且会引入极难查的 bug)。

Writing <NoteItem note={n} onDelete={handleDelete} /> is the same as calling NoteItem({ note: n, onDelete: handleDelete }). There is no magic in props — it is an ordinary object.

So that ({ note, onDelete, onEdit }) in the component is destructuring that object — the destructuring syntax from the previous course.

props are read-only. A child is not allowed to change them: note.title = "a new title" is off limits in React (TypeScript will not always stop you, but the UI will not update and you have just bought yourself a bug that is very hard to find).

§02

数据单向往下:NoteTable 只是个中转站Data goes one way, downward: NoteTable only passes it along

NoteTable。它收到 notesonDeleteonEdit 三个 props, 然后原样传给每一个 NoteItem。 它自己什么都没改。

这看起来很啰嗦 —— 为什么不让 NoteItem直接找 NoteManager 拿数据? 因为 React 里没有这条路。 数据只能一层一层往下传。这个限制听起来麻烦, 但它换来一个巨大的好处:任何时候你都能顺着 props 往上找到数据的源头。数据出错时,只需要沿着一条链排查。

Look at NoteTable. It receives three props — notes, onDelete, onEdit — and hands them straight through to every NoteItem. It changes nothing of its own.

This looks like busywork — why not let NoteItem reach up to NoteManager for the data itself? Because React has no such path. Data travels down, one level at a time. The limit sounds annoying, and it buys you something big: you can always follow props upward to where the data comes from. When a value is wrong, there is exactly one chain to walk.

TSXsrc/components/NoteTable/index.tsx(节选)src/components/NoteTable/index.tsx (extract)源项目From source
1const NoteTable: React.FC<NoteTableProps> = ({ notes, onDelete, onEdit }) => {
2 return (
3 <div className="card w-30 pt-30 pb-8 mt-2">
4 <table>
5 <thead>
6 <tr>
7 <th>Title</th>
8 <th>Content</th>
9 <th>Edit</th>
10 <th>Delete</th>
11 </tr>
12 </thead>
13 <tbody data-testid="notes-list">
14 {notes.map((note) => (
15 <NoteItem
16 key={note.id}
17 note={note}
18 onDelete={onDelete}
19 onEdit={onEdit}
20 />
21 ))}
22 </tbody>
23 </table>
24 </div>
25 );
26};
Source: react-notes-app/src/components/NoteTable/index.tsx
注意第 13 行的 data-testid="notes-list" —— 判卷测试就是靠它找到表格主体的。README 明确写了「不得修改任何 data-testid」。Look at data-testid="notes-list" on line 13. That is how the grading test finds the table body. The README says plainly that no data-testid may be changed.
§03

事件往上报:把函数当 props 传下去Events are reported upward: pass a function down as props

这是 React 里子组件影响父组件的唯一正当方式。This is the only correct way for a child to affect its parent in React.

NoteManager 里定义了 handleDelete, 然后把它作为 props 传下去NoteItem 在按钮被点时调用它。 于是发生了一件事:点击发生在最底层,state 修改发生在最顶层。

这个模式的命名习惯是:props 叫 onXxx, 处理函数叫 handleXxx。 这个项目严格遵守了它:onDeletehandleDeleteonEdithandleEditonSubmithandleSubmitNote

props 的名字是契约。父组件写onDelete={...},子组件就必须解构onDelete。写成 onRemove就对不上了 —— 而 TypeScript 会立刻报错,这是好事。

NoteManager defines handleDelete, then passes it down as a prop. NoteItem calls it when the button is clicked. Which means something worth noticing happens: the click is at the bottom, the state change is at the top.

The naming habit is: the prop is called onXxx, the handler is called handleXxx. This project follows it strictly: onDeletehandleDelete, onEdithandleEdit, onSubmithandleSubmitNote.

A prop name is a contract. The parent writes onDelete={...}, so the child has to destructure onDelete. Call it onRemove and the two no longer meet — and TypeScript complains on the spot, which is a good thing.

TSX一条完整的事件链One complete event chain源项目From source
1// NoteManager(父):定义处理函数,传下去
2const handleDelete = (id: number) => {
3 setNotes((prev) => prev.filter((note) => note.id !== id));
4};
5
6<NoteTable notes={notes} onDelete={handleDelete} onEdit={handleEdit} />
7
8// NoteItem(孙):点击时调用它
9<button onClick={() => onDelete(note.id)} className="danger">
10 Delete
11</button>
1// NoteManager (the parent): define the handler, pass it down
2const handleDelete = (id: number) => {
3 setNotes((prev) => prev.filter((note) => note.id !== id));
4};
5
6<NoteTable notes={notes} onDelete={handleDelete} onEdit={handleEdit} />
7
8// NoteItem (the grandchild): call it when the click happens
9<button onClick={() => onDelete(note.id)} className="danger">
10 Delete
11</button>
Source: react-notes-app/src/components/NoteManager/index.tsx 与 NoteItem/index.tsx
§04

onClick={fn} 和 onClick={fn()}:差一对括号,行为天差地别onClick={fn} and onClick={fn()}: one pair of parentheses apart, and the behavior is completely different

这是新手最高频的错误之一,而且症状很奇怪。This is one of the most common beginner mistakes, and the symptom looks strange.

onClick 需要的是一个函数 —— 「以后被点的时候,请调用这个」。

  • onClick={handleClick} ✓ 传的是函数本身。不需要参数时用这种。
  • onClick={() => onDelete(note.id)} ✓ 传的是一个新造的函数,它被调用时才去调onDelete(note.id)需要传参数时只能用这种。
  • onClick={onDelete(note.id)} ✗ 这是立刻调用,然后把它的返回值 (undefined)交给 onClick。 结果:组件一渲染就触发删除, 而且真正点击时毫无反应。

最后那种在这个项目里会造成什么? NoteTable渲染时,每一行都会立刻调一次 onDelete, 于是所有笔记在显示出来的瞬间就被删干净了 —— 然后 state 变了触发重渲染,再删一遍…… 很可能直接卡死。

onClick wants a function — “when this gets clicked later on, please call this”.

  • onClick={handleClick} ✓ passes the function itself. Use it when no argument is needed.
  • onClick={() => onDelete(note.id)} ✓ passes a brand-new function that only calls onDelete(note.id) once it runs. This is the only way to pass an argument.
  • onClick={onDelete(note.id)} ✗ this calls it immediately and hands the return value (undefined) to onClick. Result: the delete fires the moment the component renders, and an actual click does nothing at all.

What would the last one do in this project? While NoteTable renders, every row calls onDelete once, so every note is wiped the instant it appears — then the state change triggers another render, which deletes again... it will most likely lock up.

TSX示意Illustrative
1<button onClick={onDelete(note.id)}>Delete</button>
2{/* ↑ 渲染时就执行了。React 收到的是 undefined*/}
3
4<button onClick={() => onDelete(note.id)}>Delete</button>
5{/* ↑ 收到一个函数。点击时才执行。这才是对的。*/}
1<button onClick={onDelete(note.id)}>Delete</button>
2{/*runs during render. React receives undefined.*/}
3
4<button onClick={() => onDelete(note.id)}>Delete</button>
5{/*receives a function. It runs on click. This one is right.*/}
练习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补全 NoteItem 的两个按钮Fill in the two buttons of NoteItem

这是 NoteItem 真实的两个按钮。 一个要传整条笔记,一个只传 id —— 想清楚各自要传什么, 以及怎么才能「点击时才执行」。

These are the two real buttons of NoteItem. One passes the whole note, the other passes only the id. Decide what each one has to pass, and how to make it run only on the click.

TSXsrc/components/NoteItem/index.tsx2 个空2 blanks
1<td>
2 <button onClick={ onEdit(note)} className="outlined">
3 Edit
4 </button>
5</td>
6<td>
7 <button onClick={() => onDelete()} className="danger">
8 Delete
9 </button>
10</td>
把 2 个空都填上才能检查(还差 2 个)Fill all 2 blanks to check (2 to go)
L2Debug LabDebug LabDebug Lab · 页面一打开,所有笔记就消失了Debug Lab · every note disappears the moment the page opens

添加两条笔记后刷新页面(假设有持久化),表格瞬间变空。 有时候浏览器还会卡住。先判断类型,再找病灶。

Add two notes, then reload the page (assume the notes are saved somewhere). The table goes empty at once, and sometimes the browser stops responding. First name the kind of error, then find the line that causes 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 repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops. (另一种表现:没有任何报错,但表格永远是空的)Warning: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops. (Another symptom: no warning at all, but the table stays empty forever.)
TSX有问题的 NoteItemThe NoteItem with the bug示意Illustrative
1const NoteItem: React.FC<NoteItemProps> = ({ note, onDelete, onEdit }) => {
2 return (
3 <tr>
4 <td>{note.title}</td>
5 <td>{note.content}</td>
6 <td>
7 <button onClick={onEdit(note)} className="outlined">Edit</button>
8 </td>
9 <td>
10 <button onClick={onDelete(note.id)} className="danger">Delete</button>
11 </td>
12 </tr>
13 );
14};
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
迁移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.

子组件要影响父组件的数据A child needs to change the parent's data
父组件传一个 onXxx 函数下去The parent passes an onXxx function down
事件处理器要传参数An event handler needs an argument
包一层箭头函数 () => fn(arg)Wrap it in an arrow function: () => fn(arg)
Maximum update depth exceededMaximum update depth exceeded
先查有没有在渲染时调用了处理函数First check whether a handler is called during render
点了按钮毫无反应Clicking the button does nothing at all
查 onClick 里是不是写成了 fn() 而不是 fnCheck whether onClick says fn() instead of fn
这节的要点What to take away
  1. props 就是传给组件函数的那个对象,只读,只能从上往下传。props is the object passed to the component function. It is read-only, and it only travels downward.
  2. 子组件通过调用父组件传下来的 onXxx 函数来上报事件。A child reports an event by calling the onXxx function its parent passed down.
  3. 命名习惯:props 叫 onXxx,父组件里的实现叫 handleXxx。Naming convention: the prop is called onXxx, and the implementation in the parent is called handleXxx.
  4. 需要传参数就包一层箭头函数;onClick={fn()} 会在渲染时立刻执行。Wrap the call in an arrow function when you need to pass an argument; onClick={fn()} runs the moment the component renders.
  5. props 名字是契约,两边必须一致 —— 好在 TypeScript 会替你检查。The prop name is a contract and has to match on both sides. TypeScript checks that for you.

接下来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 lessonuseState:让界面跟着数据变useState: making the screen follow the data
    下一节Next lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: 组件就是一个返回界面的函数A component is a function that returns what you see on screen