大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
Props 与 State
这一课你将能做到
- 用 props 把数据从父传到子
- 用
useState记住会变的值,并触发界面更新 - 说出何时该「状态提升」到父组件
为什么要学
本站路径卡的标题从哪来?父页面把课名传进去——这是 props。
「离开页」输入框里你正在打的字——这是 state。
- Props:父给子的说明书(只读)
- State:组件自己的记性(会变,一变就重渲染)
分不清就会:子组件硬改 props、或两个组件各存一份「是否完成」,勾了左边右边还没勾。人话:外来参数 vs 自己记得住的东西。
先建立图像
父组件 --props--> 子组件(只读展示)
父组件 <--事件通知-- 子组件(「我被点了」)
组件内部 state:点击后 setState → React 再调用函数 → 新 JSX
| Props | State | |
|---|---|---|
| 谁给的 | 父(或默认值) | 组件自己 useState |
| 能不能直接改 | 不要改 | 用 setter |
| 变了会怎样 | 父重渲染,子收到新 props | 本组件重渲染 |
跟着做
步骤 1:props(解构写法最常见)
function StationTitle({ title }) {
return <h2>{title}</h2>;
}
export function Demo() {
return <StationTitle title="HTML 入门" />;
}
子组件里不要写 title = "别的" 去改 props。要改,让父改传进来的值。
多个属性就是多个参数:<Card title="JS" done={false} />。
步骤 2:useState(最小记忆)
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
// count:这次渲染看到的值
// setCount:更新函数;调用后安排一次重新渲染
return (
<button type="button" onClick={() => setCount(count + 1)}>
点了 {count} 次
</button>
);
}
人话:useState(0) 交给 React 一个盒子,盒子里先是 0。你只能通过 setCount 换盒子里的东西;换了 React 才愿意再画一遍。
步骤 3:更新依赖旧值时用函数式
setCount((c) => c + 1);
连续点、或在同一个事件里加两次时更稳妥。直接 setCount(count + 1) 两次可能仍只用到同一份旧 count。
步骤 4:对象/数组 state 要「换新的」
const [item, setItem] = useState({ done: false, title: "CSS" });
// 错误:item.done = true; setItem(item); // 还是同一个对象
// 正确:做出新对象
setItem({ ...item, done: true });
const [list, setList] = useState([1, 2]);
setList([...list, 3]); // 追加
setList(list.filter((x) => x !== 2)); // 删除
React 靠「是不是新引用」判断要不要更新。直接改原对象/原数组,引用没变,界面可能不刷新。
步骤 5:状态提升(两个孩子抢同一份真相)
场景:列表项和页脚都要显示「完成了几课」。
若孩子各自 useState,数字会对不上。
把 doneCount 放在共同的父组件,再 props 传下去;孩子通过 onToggle 通知父去 setState。
function Child({ done, onToggle }) {
return (
<button type="button" onClick={onToggle}>
{done ? "已完成" : "未完成"}
</button>
);
}
export function Parent() {
const [done, setDone] = useState(false);
return (
<>
<Child done={done} onToggle={() => setDone((d) => !d)} />
<p>{done ? "父也知道完成了" : "父知道未完成"}</p>
</>
);
}
步骤 6:state 更新要用新值
setCount(c => c + 1); // 连续点击更稳
// setCount(count + 1); setCount(count + 1); // 可能只 +1
讲清楚要点
- props 向下,事件向上(常见模式)。子不改共享数据,只喊「发生了点击」。
- 能算出来的别存 state(全名 = 姓+名,只存姓和名;完成数 = 列表 filter 的 length)。
useState(初始值)的初始值只在首次挂载用到。之后父传入的新 props 不会自动重置这个 state,除非你用 key 重建组件,或自己写同步(多数时候不要同步)。- 一个状态一个职责;对象很大时更新用展开,避免漏字段。
- Hooks 只能写在函数组件顶层:不要放进 if/for。
你可能会懵的地方
-
为什么不能
count++然后不管?
必须setCount,否则 React 不知道要重渲染。count++还可能改到「这次渲染的快照」,行为诡异。 -
setState 后立刻
console.log(count)还是旧值?
更新是安排下一次渲染;当下这次函数里的count还是旧快照。要看新值,看下一次渲染,或用函数式更新里的参数。 -
受控输入
输入框的value绑 state、onChange里 setState,就是受控组件(表单课)。 -
props 变了但 state 还是旧的?
正常。state 是组件自己的记忆。若「父换了一门课」应重置,给组件加key={slug}让 React 当新实例。 -
props 变了子不更新?
先确认是否真换了引用/值;再查是否错误memo或 key 稳定导致复用错位。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 改 props | 单向数据流被破坏 | 通知父改 |
| 突变数组再 set | 可能不刷新 | 新数组 [...arr] |
| 所有东西进全局 / Context | 难追更新来源 | 能本地就本地 |
| 把派生值再存一份 state | 两份数据打架 | 渲染时计算 |
| 把 props 拷进 state 再忘同步 | UI 过期 | 能算就不存;必要时 key 重置 |
自测三问
- props 和 state 的只读/可变差别?
- 为何更新对象要用展开成新对象?
- 什么叫状态提升?举一个「两个组件必须看同一份勾选」的场景。
小结一句话
props 是外来参数,state 是内部记忆;改界面就 setState,并且用新数据替换旧数据。
动手加深
做一个计数器:显示 count,按钮 +1;再加「重置」。口述:谁持有 state、谁只收 props。
延伸
- react.dev State: A Component's Memory
- 下一课:事件处理。