Skip to content
概述
React 组件函数接收外部数据(Props),管理内部可变数据(State),通过事件处理将用户行为与 State 关联,再配合条件渲染和列表渲染,便可构造出动态界面。
Props 解决“组件如何接收外部数据”,State 解决“组件如何拥有可变数据”,事件处理把两者串联起来。
Props:让数据向下流动
上一节提到组件可以像 HTML 标签一样嵌套使用。父组件通过标签属性把数据传给子组件,子组件通过函数的第一个参数接收这些属性,这就是 Props。
tsx
interface UserCardProps {
name: string;
avatar: string;
bio?: string;
}
function UserCard({ name, avatar, bio }: UserCardProps) {
return (
<div className="user-card">
<img src={avatar} alt={name} />
<h3>{name}</h3>
{bio && <p>{bio}</p>}
</div>
);
}
// 使用时
<UserCard
name="陈一"
avatar="/avatars/chenyi.png"
bio="前端,偶尔写后端"
/>Props 的关键约束是只读。子组件内部不应该修改 props.name,也不应该把 Props 重新赋值。这不是 JavaScript 运行时的强制限制,而是 React 的设计约定——数据始终从父级向子级单向流动。
如果子组件需要可以根据本地交互改变的值(例如用户点击“展开详情”后显示更多信息),这个值不应该通过修改 Props 来衍生,而应该使用 State 管理。
复合模式:Props 作为渲染数据
组件接收的 Props 可以是原始值,也可以是其他组件片段(JSX 元素)。React 用 children 属性自动接收嵌套在标签内部的内容:
tsx
function Card({ children, title }: { children: React.ReactNode; title: string }) {
return (
<div className="card">
<div className="card-header">{title}</div>
<div className="card-body">{children}</div>
</div>
);
}
// 使用
<Card title="用户信息">
<UserCard name="陈一" avatar="/avatars/chenyi.png" />
</Card>children 本质上也是 Props。这种模式让组件可以承担布局职责,而内容由外部决定。
State:为组件添加记忆
Props 在每次渲染时由父组件决定,子组件无法改变。但对于“计数器当前值”“弹窗是否打开”“表单输入内容”这类需要在交互中变化的数据,就需要 State。
React 在函数组件中通过 useState 提供状态能力。useState 是 React 内置的 Hook,调用后返回一个数组,包含当前状态值和一个更新该状态的函数:
tsx
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>当前计数:{count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
);
}调用 setCount 会触发两件事:更新 count 的值,然后安排一次重新渲染,让组件用新的 count 值重新执行。这个过程不直接操作 DOM,React 会根据状态变化计算出需要修改的最小 DOM 部分并执行更新。
useState 的行为细节
useState(initialValue) 中的初始值只在组件首次渲染时被使用。后续重新渲染时,React 已经记住了这个状态槽位,会返回当前值,忽略初始值。
tsx
function Demo() {
const [count, setCount] = useState(0); // 首次渲染时 count = 0
return <button onClick={() => setCount(count + 1)}>{count}</button>;
// 点击后 count 变为 1,重新渲染时 useState(0) 返回的是 1,不是 0
}更新函数可以接收一个新值,也可以接收一个函数。传递函数时,React 会把当前状态值作为参数传入,函数返回的值作为新状态:
tsx
setCount((prev) => prev + 1);在连续多次更新或异步回调中,使用函数形式可以避免闭包捕获到过期的状态值。
状态提升
当多个组件需要读取或改写同一份状态时,React 的解法是把状态移到它们最近的共同父组件里,然后通过 Props 把值和更新函数分别下发:
tsx
function Parent() {
const [activeId, setActiveId] = useState<string | null>(null);
return (
<div>
<Tab id="home" activeId={activeId} onSelect={setActiveId} />
<Tab id="profile" activeId={activeId} onSelect={setActiveId} />
<Panel activeId={activeId} />
</div>
);
}
function Tab({
id,
activeId,
onSelect,
}: {
id: string;
activeId: string | null;
onSelect: (id: string) => void;
}) {
return (
<button
onClick={() => onSelect(id)}
style={{ fontWeight: id === activeId ? "bold" : "normal" }}
>
{id}
</button>
);
}数据仍然单向流动:State 在 Parent 中,两个 Tab 通过 Props 接收当前选中状态和修改它的函数。这就是“状态提升”的含义——不共享状态本身,而是把状态放到需要共享的组件们的共同祖先那里。
响应用户操作:事件处理
React 在 JSX 中使用类似 HTML 属性名的方式绑定事件,但采用驼峰命名,例如 onClick、onChange、onSubmit。属性值是一个函数:
tsx
function ClickMe() {
const handleClick = () => {
console.log("clicked");
};
return <button onClick={handleClick}>点一下</button>;
}这里传递的是函数引用 handleClick,不是调用结果 handleClick()。如果写成 onClick={handleClick()},会在渲染时立即执行,而不是等到点击时才执行。
事件回调中更新 State
最典型的使用方式是在事件处理函数中调用状态更新函数:
tsx
function Toggle() {
const [visible, setVisible] = useState(false);
return (
<div>
<button onClick={() => setVisible(!visible)}>
{visible ? "收起" : "展开"}
</button>
{visible && <p>这里是详细内容。</p>}
</div>
);
}setVisible(!visible) 这种写法依赖 visible 的当前值。在快速连点或异步场景中,闭包中的 visible 可能落后于实际状态。更可靠的方式是使用函数式更新:
tsx
setVisible((prev) => !prev);合成事件对象
React 的事件处理函数接收的是 React 包装后的事件对象,即合成事件(SyntheticEvent)。它抹平了不同浏览器之间的差异,接口与原生 DOM 事件保持相似:
tsx
function InputLogger() {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
return <input onChange={handleChange} />;
}合成事件在使用后会被回收复用。如果需要异步访问事件对象(比如在 setTimeout 中读取),需要先调用 e.persist()。不过更常见的做法是提前把需要的值取出来:
tsx
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setTimeout(() => {
console.log(value); // 直接使用提取的值,不需要 e
}, 500);
};按条件展现界面
React 没有特殊的条件语法,直接使用 TypeScript 的条件语句和表达式即可。
if 语句
在组件函数体中用 if 提前返回是最直接的方式:
tsx
function Greeting({ isLoggedIn }: { isLoggedIn: boolean }) {
if (!isLoggedIn) {
return <div>请先登录</div>;
}
return <div>欢迎回来</div>;
}三元运算符
适合在 JSX 内部根据条件切换元素:
tsx
function StatusBadge({ status }: { status: "online" | "offline" }) {
return (
<span className={status === "online" ? "text-green" : "text-gray"}>
{status === "online" ? "在线" : "离线"}
</span>
);
}&& 短路
当只需要“条件成立时显示某内容,否则什么都不显示”时,用 && 最简洁:
tsx
{error && <p className="error">{error}</p>}这里利用 JavaScript 的短路求值:error 为假值时(null、undefined、""),表达式直接返回假值,React 不会渲染。
唯一需要注意的是 0 这类假值。如果条件表达式本身可能返回 0,React 会把 0 渲染到界面:
tsx
arr.length && <List />; // 当 arr.length 为 0 时,页面会显示 "0"用显式布尔转换可以避免:
tsx
arr.length > 0 && <List />;渲染列表与 key 的职责
把数组数据转为 JSX 元素列表时,使用 map 方法:
tsx
interface Todo {
id: string;
text: string;
done: boolean;
}
function TodoList({ todos }: { todos: Todo[] }) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}每个列表项都需要一个 key 属性。这个值不传给子组件作为 Props,而是供 React 在协调阶段使用——当列表发生变化时,React 通过 key 判断哪些元素可以复用,哪些需要新建或销毁。
key 不能使用数组索引
用 map 的索引作为 key 看起来很自然,但会埋下隐患:
tsx
// 不推荐
{todos.map((todo, index) => (
<li key={index}>{todo.text}</li>
))}当列表顺序发生变化(例如在头部插入一项,或排序反转)时,索引相同的项对应的是不同的数据。React 会误认为是同一项,复用状态和 DOM 节点,导致界面错位。这在含有内部状态(如输入框内容、焦点、动画)的列表项中特别明显。
key 的要求
- 在兄弟列表中必须唯一,不需要全局唯一。
- 稳定——同一数据项在多次渲染间保持相同的 key。
- 数据本身自带的 ID 通常是最合适的 key 来源;没有 ID 时可以组合几个字段生成一个唯一值。
受控组件:表单与状态绑定
HTML 表单元素(<input>、<textarea>、<select>)自身维护着内部状态:输入框里的文字、下拉选项的选中项。如果这个内部状态和 React 的 State 各自为政,就会出现两个“真相源”。
受控组件把表单的值完全交给 React 的 State 管理。以 <input type="text"> 为例:
tsx
function NameForm() {
const [name, setName] = useState("");
return (
<div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<p>你输入的是:{name}</p>
</div>
);
}input 的 value 被绑定到 State 变量 name,每次按键触发 onChange,通过 setName 更新 name,React 重新渲染后 input 显示新的 value。输入框的值不再是 DOM 自己维护的那个了——它完全受 React State 控制。
这就是“受控”的含义。把相同的模式套到其他表单元素上:
tsx
// textarea
<textarea value={text} onChange={(e) => setText(e.target.value)} />
// select
<select value={option} onChange={(e) => setOption(e.target.value)}>
<option value="a">A</option>
<option value="b">B</option>
</select>
// checkbox
<input
type="checkbox"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
/>受控组件的价值在于,所有表单数据都集中在 State 中,提交、校验、联动等逻辑不再需要直接查询 DOM。
注意点
组件名大写。React 根据首字母大小写区分组件与原生元素。
<MyButton />会被识别为组件,<button>会被识别为原生 HTML 元素。Props 不可修改。子组件收到 Props 后应该将其视为只读。如果需要基于 Props 产生可变数据,可以在组件内部使用 State 进行初始化,但要注意后续 Props 变化时是否需要同步——这属于“派生 State”的范畴,需要结合具体场景处理。
State 的更新是异步的。在同一个事件处理函数中连续调用多次
setXxx,React 不会立刻同步更新,而是在事件处理结束后批量应用。直接读取count拿到的仍然是本次渲染时的值,不是“即将更新”的值。需要基于前一个值计算时,使用函数式更新。不要在渲染期间修改 State。在组件函数体中调用
setXxx会导致无限循环——渲染触发更新,更新又触发渲染。State 更新应该放在事件处理函数或 Effect 中。key 只在列表上下文中才有意义。给孤立元素添加 key 属性不会产生任何效果。React 只在数组映射场景中使用 key 进行元素身份识别。
