React: 函数组件与 Props

最后更新:2026-08-26

函数组件就像 乐高积木——每个积木有自己的形状(参数 Props),可以反复拼装组合。Props 则是积木的"接口":你给积木什么颜色、什么尺寸,它就呈现什么样子。


1. 你将学到



2. 一个前端团队的真实故事

组件与 Props 的数据流向

100%
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>
▶ 试一试

问题:

(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>
▶ 试一试
规则 说明
首字母大写 Welcomewelcome ❌(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 activeactive={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']}
/>
逻辑代码 45 行(超过 40 行限制,仅展示)

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 />
逻辑代码 80 行(超过 40 行限制,仅展示)

▶ 示例 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>
  )
}
逻辑代码 51 行(超过 40 行限制,仅展示)

❓ 常见问题

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 表示可选,配合解构默认值更清晰。

📖 小节


📝 作业

  1. 基础题(难度⭐):创建一个 Avatar 组件,接收 name(字符串)和 size(数字,默认 50)两个 Props,显示用户头像和名字。
  2. 进阶题(难度⭐⭐):创建一个 TagList 组件,接收 tags(字符串数组)和 onRemove(函数)两个 Props,渲染标签列表,点击标签可删除。
  3. 挑战题(难度⭐⭐⭐):用 TypeScript 创建一个 DataTable 组件,接收 columns(列定义数组)和 data(数据数组),渲染一个简单的数据表格。尝试添加一个 striped(布尔值,可选)Prop 控制斑马纹。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏