Разделение компонентов — Props и колбэки событий
Компонентизация — ключевая философия проектирования React. В этой главе вы научитесь разделять страницы на небольшие переиспользуемые компоненты и реализовывать взаимодействие родителя и потомка через Props и функции обратного вызова.
Зачем разделять компоненты?
App.jsx становится всё длиннее: сотни строк нагромождены вместе, что затрудняет поддержку и повторное использование.
Принципы разделения компонентов:Каждый компонент отвечает за одну задачу, а вместе они позволяют реализовать сложную функциональность.。
Какая гранулярность уместна?
| Область страницы | Выделять ли в отдельный компонент | Причина |
|---|---|---|
| Панель навигации | Да | Нужна на каждой странице; независимая функциональность |
| Карточка статьи | Да | Каждая статья в списке имеет одинаковую структуру |
| Группа кнопок фильтрации категорий | Да | Имеет собственное состояние и взаимодействия |
| Простой контейнер-обёртка | нет | Только для разметки, без самостоятельного поведения |
От родителя к дочернему компоненту: Props
В React,Props — это параметры функции— когда родительский компонент вызывает дочерний компонент, он передаёт данные через атрибуты.
Пример
// 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-атрибутов:
Пример
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>
)
}
Пропсы — этооднонаправленный поток данных: данные передаются от родительского компонента к дочернему. Дочерний компонент не может изменять пропсы, которые получает. Если требуется изменение, родительский компонент должен обработать его через функцию обратного вызова.
От дочернего компонента к родительскому: функция обратного вызова
Когда дочернему компоненту нужно уведомить родительский компонент о том, что «что-то произошло», родительский компонент должен передатьфункцию обратного вызовав качестве пропсов.
Пример
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 CategoryFilter from './components/CategoryFilter'
function App() {
const [activeCategory, setActiveCategory] = useState('Все')
const categories = ['All', 'React', 'JavaScript', 'CSS']
// Функция обратного вызова: реагировать на событие клика дочернего компонента
function handleCategoryChange(cat) {
setActiveCategory(cat)
}
return (
<CategoryFilter
categories={categories}
activeCategory={activeCategory}
onCategoryChange={handleCategoryChange}
/>
)
}
Сравнение коммуникации между React и Vue3
| Сценарий | Vue3 | React |
|---|---|---|
| От родителя к потомку | Пропсы(defineProps) | Пропсы (параметры функции) |
| От потомка к родителю | Событие emit | Функция обратного вызова в пропсах |
| Направление данных | Односторонний | Односторонний |
Vue3 использует механизм emit, React использует колбэк-функции — суть одна и та же, различается лишь синтаксис.
Деструктуризация 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
Пример
import '../App.css'
function NavBar() {
return (
<header className="navbar">
<a href="/" className="logo">ПРИМЕР Блог</a>
<nav>
<a href="/">Главная</a>
<a href="#">О нас</a>
</nav>
</header>
)
}
export default NavBar
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: 'Изучите Flex Layout в одной статье', date: '2024-05-01', category: 'CSS' },
])
const [activeCategory, setActiveCategory] = useState('Все')
const categories = useMemo(() => {
const cats = articles.map(a => a.category)
return ['All', ...new Set(cats)]
}, [articles])
const filteredArticles = useMemo(() => {
if (activeCategory === 'All') return articles
return articles.filter(a => a.category === activeCategory)
}, [articles, activeCategory])
return (
<div className="app">
<NavBar />
<main className="container">
<h2 className="section-title">Последние статьи</h2>
{/* Дочерний компонент: получает пропсы (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 Пример блога. Работает на React.</p>
</footer>
</div>
)
}
export default App
Итоги главы
В этой главе вы освоили основу взаимодействия компонентов React: от родителя к дочернему через Props (параметры функции), от дочернего к родителю через callback-функции Props.
Три компонента проекта блога — NavBar, BlogCard и CategoryFilter — уже на месте, и структура кода стала намного понятнее.
Другие расширения