Разделение компонентов — 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>
  )
}

Пропсы — этооднонаправленный поток данных: данные передаются от родительского компонента к дочернему. Дочерний компонент не может изменять пропсы, которые получает. Если требуется изменение, родительский компонент должен обработать его через функцию обратного вызова.


От дочернего компонента к родительскому: функция обратного вызова

Когда дочернему компоненту нужно уведомить родительский компонент о том, что «что-то произошло», родительский компонент должен передатьфункцию обратного вызовав качестве пропсов.

Пример

// Путь к файлу: 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 = ['All', 'React', 'JavaScript', 'CSS']

  // Функция обратного вызова: реагировать на событие клика дочернего компонента
  function handleCategoryChange(cat) {
    setActiveCategory(cat)
  }

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

Сравнение коммуникации между React и Vue3

СценарийVue3React
От родителя к потомкуПропсы(defineProps)Пропсы (параметры функции)
От потомка к родителюСобытие emitФункция обратного вызова в пропсах
Направление данныхОдностороннийОдносторонний

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">ПРИМЕР Блог</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: 'Изучите 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 — уже на месте, и структура кода стала намного понятнее.

Другие расширения