现在位置: 首页 > React 教程 > 正文

拆分组件 — Props 与事件回调

组件化是 React 最核心的设计思想。本章你将学会把页面拆成可复用的小组件,通过 Props 和回调函数实现父子通信。


为什么要拆组件?

App.jsx 写得越来越长,上百行代码挤在一起,难以维护和复用。

拆组件的原则:每个组件只负责一件事,组合起来完成复杂功能

什么粒度合适?

页面区域是否该拆成组件理由
导航栏每个页面都需要,独立功能
文章卡片列表里每篇都是同一个结构
分类筛选按钮组有自己的状态和交互
简单包裹容器只是为了布局,没有独立行为

父传子:Props

在 React 中,Props 就是函数的参数——父组件调用子组件时,把数据通过属性传进去。

实例

// 文件路径:src/components/BlogCard.jsx
// Props 即函数的第一个参数,通常解构使用
function BlogCard({ title, summary, date, category }) {
  return (
    <div className="card">
      <span className="tag">{category}</span>
      <h3>{title}</h3>
      <p>{summary}</p>
      <span className="date">{date}</span>
    </div>
  )
}

export default BlogCard

父组件像写 HTML 属性一样把数据传进去:

实例

// 文件路径:src/App.jsx
import BlogCard from './components/BlogCard'

function App() {
  const articles = [
    { id: 1, title: 'React 入门', summary: '从零学 React', date: '2024-05-10', category: 'React' },
    { id: 2, title: 'Promise 详解', summary: '搞懂异步', date: '2024-05-08', category: 'JavaScript' },
  ]

  return (
    <div className="article-grid">
      {articles.map(article => (
        <BlogCard
          key={article.id}
          title={article.title}
          summary={article.summary}
          date={article.date}
          category={article.category}
        />
      ))}
    </div>
  )
}

Props 是单向数据流:数据从父组件流向子组件。子组件不能修改接收到的 Props。如果需要修改,应该由父组件通过回调函数来处理。


子传父:回调函数

子组件要通知父组件「发生了什么事」,父组件需要传一个回调函数作为 Props。

实例

// 文件路径:src/components/CategoryFilter.jsx
function CategoryFilter({ categories, activeCategory, onCategoryChange }) {
  return (
    <div className="filter-bar">
      {categories.map(cat => (
        <button
          key={cat}
          className={activeCategory === cat ? 'active' : ''}
          // 点击时调用父组件传来的回调函数
          onClick={() => onCategoryChange(cat)}
        >
          {cat}
        </button>
      ))}
    </div>
  )
}

export default CategoryFilter

父组件传递回调函数:

实例

import { useState } from 'react'
import CategoryFilter from './components/CategoryFilter'

function App() {
  const [activeCategory, setActiveCategory] = useState('全部')
  const categories = ['全部', 'React', 'JavaScript', 'CSS']

  // 回调函数:响应子组件的点击事件
  function handleCategoryChange(cat) {
    setActiveCategory(cat)
  }

  return (
    <CategoryFilter
      categories={categories}
      activeCategory={activeCategory}
      onCategoryChange={handleCategoryChange}
    />
  )
}

React 与 Vue3 的通信对比

场景Vue3React
父传子Props(defineProps)Props(函数参数)
子传父emit 事件Props 回调函数
数据方向单向单向

Vue3 用 emit 机制,React 用回调函数——本质一样,只是语法不同。


Props 解构与默认值

实例

// 带默认值的 Props
function BlogCard({
  title = '未命名文章',        // ES6 解构默认值
  summary = '暂无摘要',
  date = '',
  category = '未分类'
}) {
  return (
    <div className="card">
      <span className="tag">{category}</span>
      <h3>{title}</h3>
      <p>{summary}</p>
      {date && <span className="date">{date}</span>}
    </div>
  )
}

动手:拆出三个组件

博客项目的组件结构如下:

src/
├── components/
│   ├── NavBar.jsx       # 顶部导航栏
│   ├── BlogCard.jsx     # 单篇文章卡片
│   └── CategoryFilter.jsx  # 分类筛选按钮组
└── App.jsx

NavBar.jsx

实例

// 文件路径:src/components/NavBar.jsx
import '../App.css'

function NavBar() {
  return (
    <header className="navbar">
      <a href="/" className="logo">RUNOOB Blog</a>
      <nav>
        <a href="/">首页</a>
        <a href="#">关于</a>
      </nav>
    </header>
  )
}

export default NavBar

App.jsx — 整合所有组件

实例

// 文件路径:src/App.jsx
import { useState, useMemo } from 'react'
import NavBar from './components/NavBar'
import BlogCard from './components/BlogCard'
import CategoryFilter from './components/CategoryFilter'
import './App.css'

function App() {
  const [articles] = useState([
    { id: 1, title: 'React 入门完全指南', summary: '从零开始学习 React Hooks', date: '2024-05-10', category: 'React' },
    { id: 2, title: 'JS 异步编程详解', summary: '搞懂 Promise 和 async/await', date: '2024-05-08', category: 'JavaScript' },
    { id: 3, title: 'CSS Grid 布局实战', summary: '用 Grid 实现响应式布局', date: '2024-05-05', category: 'CSS' },
    { id: 4, title: 'React Hooks 深入', summary: '深入理解 useState 和 useEffect', date: '2024-05-03', category: 'React' },
    { id: 5, title: 'Flexbox 完全指南', summary: '一文学会弹性布局', date: '2024-05-01', category: 'CSS' },
  ])

  const [activeCategory, setActiveCategory] = useState('全部')

  const categories = useMemo(() => {
    const cats = articles.map(a => a.category)
    return ['全部', ...new Set(cats)]
  }, [articles])

  const filteredArticles = useMemo(() => {
    if (activeCategory === '全部') return articles
    return articles.filter(a => a.category === activeCategory)
  }, [articles, activeCategory])

  return (
    <div className="app">
      <NavBar />

      <main className="container">
        <h2 className="section-title">最新文章</h2>

        {/* 子组件:接收 props(categories, activeCategory)和回调(onCategoryChange) */}
        <CategoryFilter
          categories={categories}
          activeCategory={activeCategory}
          onCategoryChange={setActiveCategory}
        />

        <p className="result-info">{filteredArticles.length}</p>

        {filteredArticles.length === 0 ? (
          <p className="empty-tip">该分类下暂无文章</p>
        ) : (
          <div className="article-grid">
            {filteredArticles.map(article => (
              <BlogCard
                key={article.id}
                title={article.title}
                summary={article.summary}
                date={article.date}
                category={article.category}
              />
            ))}
          </div>
        )}
      </main>

      <footer className="footer">
        <p>© 2024 RUNOOB Blog. Powered by React.</p>
      </footer>
    </div>
  )
}

export default App

本章小结

本章你掌握了 React 组件通信的核心:父传子通过 Props(函数参数),子传父通过 Props 回调函数。

博客项目的三个组件 NavBar、BlogCard、CategoryFilter 已就位,代码结构清晰了很多。