React: 函数组件与 Props
最后更新:2026-08-26
函数组件就像 乐高积木——每个积木有自己的形状(参数 Props),可以反复拼装组合。Props 则是积木的"接口":你给积木什么颜色、什么尺寸,它就呈现什么样子。
1. 你将学到
- 函数组件的定义和命名规范
- Props 的传递与接收
- Props 的类型(string / number / boolean / object)
- children 特殊 Prop
- 使用 TypeScript 标注 Props 类型
2. 一个前端团队的真实故事
组件与 Props 的数据流向
flowchart TD
A[父组件] -->|"name='Alice'"| B[UserCard]
A -->|"items=[...]"| C[ProductList]
C -->|"item={...}"| D[ProductCard]
B -->|"children"| E[<Avatar />]
A -->|"onFollow回调"| B
style A fill:#e3f2fd,stroke:#1565c0
style B fill:#e8f5e9,stroke:#2e7d32
style C fill:#fff3e0,stroke:#e65100
style D fill:#fce4ec,stroke:#c62828
(1) 痛点:复制粘贴 50 遍的按钮
Charlie 是一个前端团队的技术负责人。他发现在代码仓库里,有 50 多个不同的按钮,分散在 20 个页面中:
JSX
// 页面 A:红色按钮
<button style={{ backgroundColor: 'red', color: 'white' }}>
删除
</button>
// 页面 B:蓝色按钮
<button style={{ backgroundColor: 'blue', color: 'white' }}>
提交
</button>
// 页面 C:灰色小按钮
<button style={{ backgroundColor: 'gray', color: 'black', fontSize: '12px' }}>
取消
</button>
问题:
- 修改设计:要把所有按钮改成圆角,需要改 50 个地方
- 不一致:有的按钮用
red,有的用#ff0000,有的用rgb(255,0,0) - Bug 频发:有人写错了样式,整个按钮变透明
(2) 组件化的解法
用函数组件 + Props 统一管理:
JSX
function Button({ variant = 'primary', size = 'medium', children }) {
const styles = {
primary: { backgroundColor: '#1890ff', color: 'white' },
danger: { backgroundColor: '#ff4d4f', color: 'white' },
default: { backgroundColor: '#f0f0f0', color: '#333' }
}
return (
<button style={{
...styles[variant],
padding: size === 'large' ? '12px 24px' : '8px 16px',
border: 'none',
borderRadius: '6px',
cursor: 'pointer'
}}>
{children}
</button>
)
}
// 使用:一行代码搞定
<Button variant="danger">删除</Button>
<Button variant="primary">提交</Button>
<Button variant="default">取消</Button>
收益:按钮数量从 50 个碎片 → 1 个组件,修改设计只需改一处,样式统一。
3. 函数组件基础
(1) 组件的定义
在 React 中,一个函数组件就是一个返回 JSX 的 JavaScript 函数。
JSX
// 最简单的函数组件
function Welcome() {
return <h1>Hello, React!</h1>
}
// 箭头函数写法(更常见)
const Welcome = () => {
return <h1>Hello, React!</h1>
}
// 隐式返回(只有一行 JSX 时可以省略 return)
const Welcome = () => <h1>Hello, React!</h1>
| 规则 | 说明 |
|---|---|
| 首字母大写 | Welcome ✅ welcome ❌(React 区分组件和 HTML 标签) |
| 返回 JSX | 必须返回一个 JSX 元素(或 null) |
| 纯函数 | 相同的输入(Props)必须返回相同的输出(UI) |
| 不可修改 Props | Props 是只读的,组件内部不能修改 |
(2) Props 是函数的参数
Props(Properties 的缩写)是父组件传给子组件的只读数据。
JSX
// ---- 定义组件 ----
function Greeting(props) {
// props 是一个对象,包含所有传入的参数
return <h1>你好,{props.name}!你今年 {props.age} 岁了。</h1>
}
// ---- 使用组件 ----
<Greeting name="Alice" age={28} />
// 渲染结果:
// <h1>你好,Alice!你今年 28 岁了。</h1>
(3) Props 的解构写法
更常用的写法是解构赋值,直接拿到需要的参数:
JSX
// 解构写法(推荐)
function Greeting({ name, age }) {
return <h1>你好,{name}!你今年 {age} 岁了。</h1>
}
// 相当于:
function Greeting(props) {
const { name, age } = props
return <h1>你好,{name}!你今年 {age} 岁了。</h1>
}
4. Props 的类型
| Props 类型 | 传递语法 | 接收示例 | 注意事项 |
|---|---|---|---|
| 字符串 | name="Alice" |
{ name } → "Alice" |
双引号包裹 |
| 数字 | age={30} |
{ age } → 30 |
必须用 {} |
| 布尔值 | active / active={true} |
{ active } → true |
省略值默认 true |
| 对象 | info={{ key: 'val' }} |
{ info } → { key: 'val' } |
双花括号 |
| 数组 | items={[1, 2, 3]} |
{ items } → [1,2,3] |
必须用 {} |
| 函数 | onClick={handler} |
{ onClick } → fn |
回调/事件处理 |
| 组件 | icon={<Icon />} |
{ icon } → JSX |
动态渲染组件 |
Props 可以传递任何 JavaScript 数据类型:字符串、数字、布尔值、对象、数组、函数,甚至其他组件。
(1) 字符串
字符串可以直接用双引号传递(就像 HTML 属性):
JSX
function UserCard({ name, role }) {
return (
<div className="card">
<h2>{name}</h2>
<p>角色:{role}</p>
</div>
)
}
// 字符串用双引号,其他类型用 {} 包裹
<UserCard name="Alice" role="管理员" />
(2) 数字、布尔值、对象
非字符串类型必须用 {} 包裹:
JSX
function Product({ name, price, inStock, details }) {
return (
<div className="product">
<h3>{name}</h3>
<p>价格:${price}</p>
{/* 布尔值用于条件渲染 */}
{inStock ? <span>有货 ✅</span> : <span>缺货 ❌</span>}
{/* 对象:可以访问属性 */}
<p>类别:{details.category} | 重量:{details.weight}g</p>
</div>
)
}
// 使用组件
<Product
name="无线鼠标"
price={89}
inStock={true}
details={{ category: '外设', weight: 120 }}
/>
(3) 传值方式速查表
| 类型 | 示例 | 说明 |
|---|---|---|
| 字符串 | name="Alice" |
和 HTML 属性写法一样 |
| 数字 | count={42} |
需要用 {} |
| 布尔值 true | active 或 active={true} |
只写属性名默认为 true |
| 布尔值 false | active={false} |
需要用 {} |
| 对象 | data={{ x:1, y:2 }} |
双花括号 |
| 数组 | items={[1,2,3]} |
需要用 {} |
| 表达式 | result={a + b} |
任意 JS 表达式 |
| 变量 | name={userName} |
传递变量 |
▶ 示例:用户信息卡片
JSX
📖 仅展示
// ============================================
// 示例:用 Props 构建一个用户信息卡片
// 功能:展示 Props 的多种类型传递
// ============================================
function UserCard({ name, age, isOnline, avatar, tags }) {
return (
<div style={{
border: '1px solid #ddd',
borderRadius: '8px',
padding: '20px',
maxWidth: '300px'
}}>
<img
src={avatar}
alt={name}
style={{ width: '80px', height: '80px', borderRadius: '50%' }}
/>
<h2>{name}</h2>
<p>年龄:{age} 岁</p>
<p>
状态:
<span style={{ color: isOnline ? '#52c41a' : '#999' }}>
{isOnline ? '在线' : '离线'}
</span>
</p>
<div>
标签:
{tags.map(tag => (
<span key={tag} style={{
background: '#f0f0f0',
padding: '2px 8px',
borderRadius: '4px',
margin: '0 4px',
fontSize: '12px'
}}>
{tag}
</span>
))}
</div>
</div>
)
}
// 使用组件
<UserCard
name="Alice"
age={28}
isOnline={true}
avatar="https://i.pravatar.cc/80"
tags={['前端', 'React', 'TypeScript']}
/>
5. children 与默认值
(1) children:组件内的嵌套内容
children 是一个特殊的 Prop,代表组件标签之间的内容。
JSX
// 定义:用 children 接收标签间的内容
function Card({ children, title }) {
return (
<div style={{ border: '1px solid #ddd', padding: '16px', borderRadius: '8px' }}>
<h3>{title}</h3>
<div>{children}</div> {/* 标签之间的内容在这里显示 */}
</div>
)
}
// 使用:标签之间的内容自动成为 children
<Card title="公告">
<p>系统将于今晚 22:00 进行维护,预计持续 2 小时。</p>
<p>请提前保存您的工作,以免数据丢失。</p>
</Card>
// 渲染结果:
// <div class="card">
// <h3>公告</h3>
// <div>
// <p>系统将于今晚 22:00 进行维护...</p>
// <p>请提前保存您的工作...</p>
// </div>
// </div>
(2) 默认值
使用 ES6 解构默认值语法,给 Props 设置默认值:
JSX
function Button({ text = '点击', color = 'blue', size = 'medium' }) {
return (
<button style={{
backgroundColor: color,
padding: size === 'large' ? '12px 24px' : '8px 16px',
color: 'white',
border: 'none',
borderRadius: '4px'
}}>
{text}
</button>
)
}
// 不传参 → 使用默认值
<Button /> // 蓝色、medium、"点击"
// 传部分参数 → 覆盖默认值
<Button text="提交" /> // 蓝色、medium、"提交"
<Button color="red" size="large" /> // 红色、large、"点击"
6. TypeScript 类型标注(简要)
使用 TypeScript 可以为 Props 定义严格的类型,让编辑器自动提示和检查:
TSX
// ---- 定义 Props 类型 ----
interface ButtonProps {
text: string // 必填:string
color?: string // 选填:加 ? 号
size?: 'small' | 'medium' | 'large' // 选填:限制为三个值之一
onClick: () => void // 必填:函数类型
disabled?: boolean // 选填:布尔值,默认为 false
}
// ---- 在组件中使用 ----
function Button({ text, color = 'blue', size = 'medium', onClick, disabled = false }: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
style={{
backgroundColor: color,
padding: size === 'large' ? '12px 24px' : '8px 16px',
opacity: disabled ? 0.5 : 1,
color: 'white',
border: 'none',
borderRadius: '4px'
}}
>
{text}
</button>
)
}
// 使用:编辑器会智能提示
<Button
text="保存"
color="#1890ff"
size="medium"
onClick={() => alert('保存成功!')}
/>
// ❌ 错误:TypeScript 会报错
<Button text="测试" /> // 缺少必填的 onClick
<Button text="测试" size="xl" onClick={fn} /> // size 不在允许的范围内
7. 完整示例:商品卡片组件
TSX
// ============================================
// 示例:商品卡片组件(综合运用 Props 各种类型)
// ============================================
// 类型定义
interface Product {
id: number
name: string
price: number
image: string
category: string
tags: string[]
}
interface ProductCardProps {
product: Product
onAddToCart: (productId: number) => void
showDiscount?: boolean
size?: 'small' | 'large'
}
// 组件实现
function ProductCard({
product,
onAddToCart,
showDiscount = false,
size = 'large'
}: ProductCardProps) {
return (
<div style={{
border: '1px solid #eee',
borderRadius: '8px',
padding: size === 'large' ? '16px' : '10px',
width: size === 'large' ? '280px' : '200px'
}}>
<img
src={product.image}
alt={product.name}
style={{ width: '100%', height: '150px', objectFit: 'cover' }}
/>
<h3>{product.name}</h3>
<p style={{ color: '#ff4d4f', fontSize: '20px', fontWeight: 'bold' }}>
${product.price}
{showDiscount && <span style={{ fontSize: '12px', color: '#999', textDecoration: 'line-through', marginLeft: '8px' }}>${(product.price * 1.2).toFixed(0)}</span>}
</p>
<p style={{ color: '#666', fontSize: '14px' }}>{product.category}</p>
<div>
{product.tags.map(tag => (
<span key={tag} style={{ background: '#f0f5ff', color: '#1890ff', padding: '2px 6px', borderRadius: '4px', margin: '0 2px', fontSize: '12px' }}>
{tag}
</span>
))}
</div>
<button
onClick={() => onAddToCart(product.id)}
style={{
width: '100%',
padding: '8px',
marginTop: '10px',
backgroundColor: '#1890ff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
加入购物车
</button>
</div>
)
}
// 使用组件
const myProduct = {
id: 101,
name: '机械键盘',
price: 299,
image: 'https://via.placeholder.com/280x150',
category: '电脑外设',
tags: ['热卖', '新品']
}
<ProductCard
product={myProduct}
onAddToCart={(id) => console.log('加入购物车:', id)}
showDiscount={true}
/>
预期输出:一张商品卡片,显示键盘图片、名称「机械键盘」、价格 $299(划线原价 $359)、类别「电脑外设」、标签「热卖」「新品」,下方有蓝色"加入购物车"按钮。
▶ 示例 2:Props 类型传递与默认值综合运用
JSX
📖 仅展示
// ============================================
// 示例:个人资料卡片——Props 类型与默认值综合运用
// 功能:展示 string、number、boolean、object、function 等 Props 类型的传递
// ============================================
function ProfileCard({
name = '匿名用户',
age = 0,
isActive = false,
avatar = 'https://via.placeholder.com/60',
skills = [],
onGreet
}) {
return (
<div style={{
border: '1px solid #e8e8e8',
borderRadius: '12px',
padding: '20px',
maxWidth: '320px',
boxShadow: '0 2px 8px rgba(0,0,0,0.06)'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px', marginBottom: '16px' }}>
<img
src={avatar}
alt={name}
style={{ width: '60px', height: '60px', borderRadius: '50%', objectFit: 'cover' }}
/>
<div>
<h3 style={{ margin: 0 }}>{name}</h3>
<p style={{ margin: '4px 0 0 0', color: '#666', fontSize: '14px' }}>
{age} 岁 · {isActive ? '🟢 在线' : '🔴 离线'}
</p>
</div>
</div>
{skills.length > 0 && (
<div style={{ marginBottom: '12px' }}>
<p style={{ margin: '0 0 8px 0', fontWeight: 'bold', fontSize: '14px' }}>技能:</p>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
{skills.map((skill, index) => (
<span key={index} style={{
backgroundColor: '#f0f5ff',
color: '#1890ff',
padding: '4px 10px',
borderRadius: '12px',
fontSize: '12px'
}}>
{skill}
</span>
))}
</div>
</div>
)}
{onGreet && (
<button
onClick={() => onGreet(name)}
style={{
width: '100%',
padding: '8px',
backgroundColor: '#1890ff',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '14px'
}}
>
向 {name} 打招呼
</button>
)}
</div>
)
}
// 使用:全部传参
<ProfileCard
name="Alice"
age={28}
isActive={true}
avatar="https://i.pravatar.cc/60"
skills={['React', 'TypeScript', 'Node.js']}
onGreet={(name) => alert(`你好,${name}!`)}
/>
// 使用:只传部分参数(其余使用默认值)
<ProfileCard
name="Bob"
skills={['Vue', 'Python']}
/>
// 使用:不传任何参数(全部使用默认值)
<ProfileCard />
▶ 示例 3:事件回调 Props——子组件通知父组件
JSX
function AddToCartButton({ productName, onAdd }) {
const [adding, setAdding] = useState(false)
async function handleClick() {
setAdding(true)
await onAdd(productName)
setAdding(false)
}
return (
<button onClick={handleClick} disabled={adding}
style={{ padding: '8px 16px', cursor: adding ? 'wait' : 'pointer' }}>
{adding ? 'Adding...' : `Add ${productName}`}
</button>
)
}
function ProductPage() {
const [cart, setCart] = useState([])
function handleAdd(name) {
setCart(prev => [...prev, name])
}
return (
<div>
<h2>Products</h2>
<AddToCartButton productName="Keyboard" onAdd={handleAdd} />
<AddToCartButton productName="Mouse" onAdd={handleAdd} />
<p>Cart: {cart.join(', ') || 'empty'}</p>
</div>
)
}
▶ 示例 4:条件渲染 Props——动态组件切换
JSX
function StatusMessage({ status }) {
const config = {
loading: { icon: '⏳', color: '#1890ff', text: 'Loading...' },
success: { icon: '✅', color: '#52c41a', text: 'Success!' },
error: { icon: '❌', color: '#ff4d4f', text: 'Error occurred' },
empty: { icon: '📭', color: '#999', text: 'No data' },
}
const { icon, color, text } = config[status] || config.empty
return (
<div style={{ padding: 16, textAlign: 'center', color }}>
<span style={{ fontSize: 32 }}>{icon}</span>
<p>{text}</p>
</div>
)
}
function DataViewer() {
const [status, setStatus] = useState('loading')
return (
<div style={{ maxWidth: 300, margin: '0 auto' }}>
<StatusMessage status={status} />
<div style={{ display: 'flex', gap: 4 }}>
{['loading', 'success', 'error', 'empty'].map(s => (
<button key={s} onClick={() => setStatus(s)}
style={{ padding: '4px 8px', cursor: 'pointer', fontSize: 12 }}>
{s}
</button>
))}
</div>
</div>
)
}
▶ 示例 5:renderProps 模式——列表组件与自定义渲染
JSX
📖 仅展示
function DataList({ items, renderItem, keyExtractor, emptyMessage = 'No items' }) {
if (!items || items.length === 0) {
return <p style={{ color: '#999', textAlign: 'center', padding: 20 }}>{emptyMessage}</p>
}
return (
<ul style={{ listStyle: 'none', padding: 0 }}>
{items.map(item => (
<li key={keyExtractor(item)} style={{ borderBottom: '1px solid #f0f0f0', padding: 8 }}>
{renderItem(item)}
</li>
))}
</ul>
)
}
function App() {
const users = [
{ id: 1, name: 'Alice', email: 'alice@example.com', active: true },
{ id: 2, name: 'Bob', email: 'bob@example.com', active: false },
{ id: 3, name: 'Charlie', email: 'charlie@example.com', active: true },
]
const products = [
{ id: 'a', name: 'Keyboard', price: 79 },
{ id: 'b', name: 'Monitor', price: 399 },
]
return (
<div style={{ maxWidth: 400, margin: '0 auto' }}>
<h3>Users</h3>
<DataList
items={users}
keyExtractor={u => u.id}
renderItem={u => (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>{u.name} ({u.email})</span>
<span style={{ color: u.active ? '#52c41a' : '#999' }}>
{u.active ? 'Active' : 'Inactive'}
</span>
</div>
)}
/>
<h3>Products</h3>
<DataList
items={products}
keyExtractor={p => p.id}
emptyMessage="No products available"
renderItem={p => (
<span>{p.name} - ${p.price}</span>
)}
/>
</div>
)
}
❓ 常见问题
Q 为什么 Props 是只读的?不能直接修改吗?
A React 的设计哲学是"单向数据流"——数据从父组件流向子组件,子组件不能修改父组件的数据。如果子组件需要修改数据,应该通过 Props 传入的回调函数通知父组件去修改(保持"单一数据源")。这样数据流向清晰,调试和维护都更容易。
Q Props 和 State 有什么区别?
A Props 是外部传入的、只读的;State 是组件内部管理的、可变的。可以这样理解:Props 是组件的"参数配置",State 是组件的"内部状态"。Props 变了 → 组件重新渲染;State 变了 → 组件重新渲染。
Q 组件名必须大写吗?小写会怎样?
A 必须大写。React 通过首字母大小写来区分组件和 HTML 标签:
<div> 是 HTML div 标签;<Div> 是自定义组件。如果组件用小写,React 会把它当作 HTML 标签处理,导致渲染失败。Q 组件可以嵌套多深?有没有限制?
A 没有技术限制,但建议不超过 3-4 层。过深的嵌套会让数据传递变得复杂(Props Drilling),可读性也变差。如果需要跨多层传递数据,可以考虑使用 Context API(第 13 课)或全局状态管理(第 18 课)。
Q Props 的默认值怎么设置?
A 两种方式:① 解构时赋默认值
function Card({ title = 'Untitled' }) {};② 用 Card.defaultProps = { title: 'Untitled' }。注意 React 官方已不推荐 defaultProps(函数组件中),建议用解构默认值。TypeScript 中还可以用 title?: string 表示可选,配合解构默认值更清晰。📖 小节
- 函数组件就是一个返回 JSX 的 JavaScript 函数,首字母必须大写
- Props 是父组件传给子组件的只读数据,子组件不能修改
- Props 可以传递任何类型:string、number、boolean、object、array、function
children是特殊的 Prop,代表组件标签之间的内容- 使用解构语法
function Comp({ prop1, prop2 })让代码更清晰 - TypeScript 类型标注可以大大提升开发体验和代码健壮性
📝 作业
- 基础题(难度⭐):创建一个
Avatar组件,接收name(字符串)和size(数字,默认 50)两个 Props,显示用户头像和名字。 - 进阶题(难度⭐⭐):创建一个
TagList组件,接收tags(字符串数组)和onRemove(函数)两个 Props,渲染标签列表,点击标签可删除。 - 挑战题(难度⭐⭐⭐):用 TypeScript 创建一个
DataTable组件,接收columns(列定义数组)和data(数据数组),渲染一个简单的数据表格。尝试添加一个striped(布尔值,可选)Prop 控制斑马纹。