第 3 章:组件与 Props
学习目标
- 理解组件是 React 应用的最小单元
- 掌握函数组件的定义、组合、嵌套
- 熟练使用 Props(类型、默认值)
- 区分 Props 与 State,避开 4 个常见坑
一、组件是什么?
组件 = 返回 JSX 的函数(函数组件是主流)。把 UI 拆成独立、可复用的片段。
tsx
function Welcome(props: { name: string }) {
return <h1>你好,{props.name}</h1>;
}特点:
- 大写开头(必须!)
- 返回单个根元素(或 Fragment)
- 可以被其他组件引用
二、定义组件
函数组件(推荐)
tsx
import type { ReactNode } from 'react';
interface CardProps {
title: string;
children: ReactNode; // 插槽
}
function Card({ title, children }: CardProps) {
return (
<div className="card">
<h3>{title}</h3>
<div className="card-body">{children}</div>
</div>
);
}箭头函数(简洁)
tsx
const Card = ({ title, children }: CardProps) => (
<div className="card">
<h3>{title}</h3>
<div className="card-body">{children}</div>
</div>
);⚠️ 坑 1:箭头函数组件在 DevTools 里显示为
Anonymous,调试不方便,复杂组件用function声明。
三、使用组件
tsx
function App() {
return (
<div>
<Card title="用户协议">
<p>请仔细阅读本协议...</p>
</Card>
<Card title="隐私政策">
<p>我们如何保护你的数据...</p>
</Card>
</div>
);
}<Card> 之间夹的内容会自动作为 children 传入,这就是插槽机制。
四、Props 详解
基础类型
tsx
interface UserCardProps {
name: string; // 必填
age?: number; // 可选(?)
role: 'admin' | 'user'; // 字面量联合
onClick: () => void; // 回调函数
data: { id: number; name: string }; // 对象
}
function UserCard({ name, age = 18, role, onClick, data }: UserCardProps) {
return (
<div onClick={onClick}>
{name}({age}) - {role} - {data.id}
</div>
);
}默认值
tsx
function Button({ type = 'primary', disabled = false, children }: ButtonProps) {
return <button className={`btn btn-${type}`} disabled={disabled}>{children}</button>;
}展开传参
tsx
const props = { type: 'danger', disabled: true, children: '删除' };
<Button {...props} />children 不止是元素
tsx
<Form>
<input />
<button>提交</button>
{() => console.log('函数 children')} {/* 渲染函数,高级用法 */}
</Form>五、组合 vs 继承
React 推荐组合,反对继承。需要复用逻辑用:
- 组件嵌套(插槽)
- 自定义 Hook(第 15 章)
- Render Props
tsx
// 错误:继承(React 不推荐)
class MyButton extends Button {}
// 正确:组合
function Dialog({ title, body, footer }: DialogProps) {
return (
<div className="dialog">
<h2>{title}</h2>
<div>{body}</div>
<div className="footer">{footer}</div>
</div>
);
}
// 使用
<Dialog
title="提示"
body={<p>确定删除?</p>}
footer={<><button onClick={cancel}>取消</button><button onClick={ok}>确定</button></>}
/>六、Props 是只读的
函数组件不能修改 props:
tsx
function Bad({ count }: { count: number }) {
count = count + 1; // ⚠️ TypeScript 报错,React 不允许
return <div>{count}</div>;
}要改值,得用 State(第 4 章)。
⚠️ 坑 2:不要在子组件里改 props,要改就触发父组件的回调(
onChange(newValue)),让父组件更新 state 再传下来。
七、key 与 props 的关系
渲染列表时 React 用 key 匹配前后元素,不要把信息塞进 key:
tsx
// 错误:用 index 当 key,顺序变就乱
{items.map((item, i) => <Item key={i} {...item} />)}
// 正确:用业务 ID
{items.map(item => <Item key={item.id} {...item} />)}⚠️ 坑 3:同一层级兄弟元素的 key 必须唯一,但不同父组件可以有相同 key。
八、本章小结
| 要点 | 关键 |
|---|---|
| 组件定义 | 函数返回 JSX,大写开头 |
| Props 类型 | 用 interface 定义,可选用 ? |
| 默认值 | 解构时 =,或 defaultProps(旧) |
| children | 自动接收标签之间内容 |
| 组合 vs 继承 | 优先组合(嵌套 / 插槽) |
| Props 只读 | 不能在子组件修改,只能通过回调上报 |
动手练习
- Card 组件:写
<Card title="...">{children}</Card>,传入不同内容测试 - Avatar 组件:接收
src、size、alt,用对象解构 + 默认值 - 可配置 Button:支持
type(primary|danger|default)、disabled、onClick,传不同 props 看效果 - 找 bug:下面代码哪里错?tsx
function Item({ text }: { text: string }) { text = text.toUpperCase(); return <li>{text}</li>; }
下一章:第 4 章:State →