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.
这一页有什么On this page6
- 01 props 就是函数参数props are just function arguments
- 02 数据单向往下:NoteTable 只是个中转站Data goes one way, downward: NoteTable only passes it along
- 03 事件往上报:把函数当 props 传下去Events are reported upward: pass a function down as props
- 04 onClick={fn} 和 onClick={fn()}:差一对括号,行为天差地别onClick={fn} and onClick={fn()}: one pair of parentheses apart, and the behavior is completely different
- 练习 · 动手做Practice
- 迁移模式Transfer
- 说清 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
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.
react-notes-app/src/components/NoteTable/index.tsx把 props 原样往下传Passes props straight down
react-notes-app/src/components/NoteTable/index.tsxreact-notes-app/src/components/NoteItem/index.tsx调用 props 里的函数上报Reports upwards by calling a function from props
react-notes-app/src/components/NoteItem/index.tsxprops 就是函数参数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).
数据单向往下:NoteTable 只是个中转站Data goes one way, downward: NoteTable only passes it along
看 NoteTable。它收到 notes、onDelete、onEdit 三个 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.
react-notes-app/src/components/NoteTable/index.tsx事件往上报:把函数当 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。 这个项目严格遵守了它:onDelete ← handleDelete、onEdit ← handleEdit、onSubmit ← handleSubmitNote。
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: onDelete ← handleDelete, onEdit ← handleEdit, onSubmit ← handleSubmitNote.
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.
react-notes-app/src/components/NoteManager/index.tsx 与 NoteItem/index.tsxonClick={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 callsonDelete(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.
动手做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.
这是 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.
添加两条笔记后刷新页面(假设有持久化),表格瞬间变空。 有时候浏览器还会卡住。先判断类型,再找病灶。
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.
换一道题也能用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.
- props 就是传给组件函数的那个对象,只读,只能从上往下传。props is the object passed to the component function. It is read-only, and it only travels downward.
- 子组件通过调用父组件传下来的 onXxx 函数来上报事件。A child reports an event by calling the onXxx function its parent passed down.
- 命名习惯:props 叫 onXxx,父组件里的实现叫 handleXxx。Naming convention: the prop is called onXxx, and the implementation in the parent is called handleXxx.
- 需要传参数就包一层箭头函数;onClick={fn()} 会在渲染时立刻执行。Wrap the call in an arrow function when you need to pass an argument; onClick={fn()} runs the moment the component renders.
- props 名字是契约,两边必须一致 —— 好在 TypeScript 会替你检查。The prop name is a contract and has to match on both sides. TypeScript checks that for you.