拆分组件 — 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
// 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>
)
}
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
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}
/>
)
}
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 的通信对比
| 场景 | Vue3 | React |
|---|---|---|
| 父传子 | 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>
)
}
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
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
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 已就位,代码结构清晰了很多。
