Глобальное состояние — Context + useReducer

В этой главе вы узнаете, как использовать React Context + useReducer для управления общими данными между компонентами и страницами, а также реализовать функцию «избранные статьи».


Зачем нам нужно глобальное состояние?

До сих пор данные передаются слой за слоем через Props. Но некоторые данные должны быть доступны нескольким несвязанным компонентам:

  • Список избранного должен быть общим для главной страницы, страницы с подробностями и панели навигации
  • Все страницы должны знать статус входа пользователя
  • Данные корзины должны быть доступны на всех страницах

Передача Props слой за слоем (Prop Drilling)Проблема: A → B → C → D, промежуточные компоненты вынуждены получать Props, которые они не используют.

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


Основные концепции контекста

ШагиCodeНазначение
Создание контекстаcreateContext()Создайте общий контейнер данных
Предоставление данных<Provider value={...}>Оберните верхнюю часть дерева компонентов и внедрите данные
Использование данныхuseContext(MyContext)Чтение данных в любом компоненте-потомке

createContext и Provider

Пример

import { createContext, useContext, useState } from 'react'

// Шаг 1: Создание контекста
const ThemeContext = createContext(null)

// Шаг 2: Создайте компонент Provider
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')
  const toggleTheme = () => setTheme(theme === 'light' ? 'dark' : 'light')

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

// Шаг 3: Используйте это в любом компоненте
function ThemedButton() {
  const { theme, toggleTheme } = useContext(ThemeContext)
  return (
    <button onClick={toggleTheme}>
Текущая тема:{theme}
    </button>
  )
}

// Шаг 4: Оберните Provider в App
function App() {
  return (
    <ThemeProvider>
      <ThemedButton />
    </ThemeProvider>
  )
}

Context похож на «вещательную башню»: Provider транслирует данные наружу, а useContext получает данные.


useReducer — Управление сложным состоянием

Когда логика состояния становится сложной (множество подсостояний, множество методов обновления), useState не справляется.

useReducerОн предоставляет шаблон управления состоянием, похожий на Redux:dispatch action → reducer вычисляет новое состояние。

Пример

import { useReducer } from 'react'

// функция reducer: (текущее состояние, action) ⇒ новое состояние
function favoriteReducer(state, action) {
  switch (action.type) {
    case 'TOGGLE':
      // Удалить из избранного, если уже в избранном, иначе добавить
      if (state.includes(action.id)) {
        return state.filter(id => id !== action.id)
      } else {
        return [...state, action.id]
      }
    case 'CLEAR':
      return []
    default:
      return state
  }
}

function FavoriteDemo() {
  // useReducer(reducer, начальное значение)
  const [favoriteIds, dispatch] = useReducer(favoriteReducer, [])

  return (
    <div>
      <p>Количество избранных:{favoriteIds.length}</p>
      <button onClick={() => dispatch({ type: 'TOGGLE', id: 1 })}>
        {favoriteIds.includes(1) ? 'Unfavorite 1' : 'Избранное 1'}
      </button>
      <button onClick={() => dispatch({ type: 'ОЧИСТИТЬ' })}>Очистить избранное</кнопка>
    </div>
  )
}

useState против useReducer

СценарийРекомендацияПричина
Одно независимое состояниеuseStateПростой и прямой
Несколько взаимосвязанных состоянийuseReducerОдин dispatch может обновлять несколько состояний
Сложная логика обновленияuseReducerУ редьюсера понятные типы действий, что упрощает отладку
Нужно передавать глубоко вложенным компонентамuseState или useReducerОба можно использовать с Context

Комбинированный паттерн Context + useReducer

Это самое мощное решение для управления глобальным состоянием в React, а также основная идея Redux.

Пример

// Путь к файлу: src/context/FavoriteContext.jsx
import { createContext, useContext, useReducer, useEffect } from 'react'

// 1. Создание Context
const FavoriteContext = createContext(null)

// 2. Определение reducer
function favoriteReducer(state, action) {
  switch (action.type) {
    case 'TOGGLE':
      if (state.includes(action.id)) {
        return state.filter(id => id !== action.id)
      } else {
        return [...state, action.id]
      }
    case 'CLEAR':
      return []
    default:
      return state
  }
}

// 3. Компонент Provider: объедините Context + useReducer
export function FavoriteProvider({ children }) {
  // Восстановить исходное состояние из localStorage
  const [favoriteIds, dispatch] = useReducer(favoriteReducer, [], () => {
    const saved = localStorage.getItem('blog-favorites')
    return saved ? JSON.parse(saved) : []
  })

  // Автосинхронизация с localStorage при изменении списка избранного
  useEffect(() => {
    localStorage.setItem('blog-favorites', JSON.stringify(favoriteIds))
  }, [favoriteIds])

  // Обернуть несколько распространённых методов
  function toggleFavorite(id) {
    dispatch({ type: 'TOGGLE', id })
  }

  function isFavorite(id) {
    return favoriteIds.includes(id)
  }

  const value = {
    favoriteIds,
    favoriteCount: favoriteIds.length,
    toggleFavorite,
    isFavorite
  }

  return (
    <FavoriteContext.Provider value={value}>
      {children}
    </FavoriteContext.Provider>
  )
}

// 4. Пользовательский хук: обернуть useContext, чтобы сделать вызывающий код чище
export function useFavorites() {
  const context = useContext(FavoriteContext)
  if (!context) {
    throw new Error('useFavorites должен использоваться внутри FavoriteProvider')
  }
  return context
}

Регистрация Provider в main.jsx

Пример

// Путь к файлу: src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider } from 'react-router-dom'
import { FavoriteProvider } from './context/FavoriteContext'
import router from './router'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    {/* Оберните Provider на самом внешнем уровне, чтобы все компоненты могли получить к нему доступ */}
    <FavoriteProvider>
      <RouterProvider router={router} />
    </FavoriteProvider>
  </React.StrictMode>
)

Почему Provider размещён вне RouterProvider? Потому что NavBar (в App) также должен читать состояние избранного. Provider должен обернуть все компоненты, которым нужен доступ к этому состоянию.


Использование функции «Избранное» в компонентах

Кнопка «Избранное» в BlogCard

Пример

// Путь к файлу: src/components/BlogCard.jsx
import { Link } from 'react-router-dom'
import { useFavorites } from '../context/FavoriteContext'

function BlogCard({ id, title, summary, date, category }) {
  const { isFavorite, toggleFavorite } = useFavorites()

  function handleFavorite(e) {
    e.preventDefault()   // Предотвратить переход по ссылке Link
    toggleFavorite(id)
  }

  return (
    <Link to={`/post/${id}`} className="card-link">
      <div className="card">
        <span className="tag">{category}</span>
        <h3>{title}</h3>
        <p>{summary}</p>
        <div className="card-footer">
          <span className="date">{date}</span>
          <button className="fav-btn" onClick={handleFavorite}>
            {isFavorite(id) ? '&#x2665;' : '♡'}
          </button>
        </div>
      </div>
    </Ссылка>
  )
}

export default BlogCard

Кнопка избранного в PostPage

Пример

// Путь к файлу: src/pages/PostPage.jsx (ключевая часть)
import { useFavorites } from '../context/FavoriteContext'

function PostPage() {
  // ... остальной код
  const { isFavorite, toggleFavorite } = useFavorites()

  return (
    <article className="post-view">
      <div className="post-header">
        <h1>{article.title}</h1>
        <button className="fav-btn" onClick={() => toggleFavorite(article.id)}>
          {isFavorite(article.id) ? '&#x2665; В избранном' : '♡ В избранное'}
        </button>
      </div>
      {/* ... */}
    </article>
  )
}

Отображение количества избранного в NavBar

Пример

// Путь к файлу: src/components/NavBar.jsx
import { useFavorites } from '../context/FavoriteContext'

function NavBar() {
  const { favoriteCount } = useFavorites()

  return (
    <header className="navbar">
      <a href="/" className="logo">ПРИМЕР Блог</a>
      <nav>
        <a href="/">Главная</a>
        {favoriteCount > 0 && (
          <span className="fav-badge">Избранное{favoriteCount}</span>
        )}
      </nav>
    </header>
  )
}

После добавления статьи в избранное на главной странице страница сведений автоматически показывает «Добавлено в избранное», а число в панели навигации синхронно обновляется — в этом ценность глобального совместного использования Context.


Context + useReducer против Vue3 Pinia

ХарактеристикаReact Context + useReducerVue3 Pinia
Способ интеграцииВстроено в React, установка не требуетсяОтдельная библиотека, требуется установка
Определение состоянияНачальное значение useReducerstate: () => ({})
Обновление состоянияdispatch({ type, payload })Прямое изменение или функции действий
Вычисляемые свойстваРучное использование useMemoгеттеры
Подход к реактивностиИзменения value в Provider вызывают повторный рендерингАвтоматическое отслеживание зависимостей
DevToolsНет встроенной поддержки (требуется Redux DevTools)Нативная поддержка Vue DevTools

Краткое содержание главы

В этой главе вы освоили полное решение для глобального управления состоянием в React: createContext создает контейнер, Provider внедряет данные, useContext потребляет данные, а useReducer управляет сложной логикой обновления.

Функция «избранные статьи» поддерживает глобальную синхронизацию данных на главной странице, странице с подробной информацией и в панели навигации, и её можно восстановить через localStorage после обновления страницы.

Дополнительные темы