Глобальное состояние — Context + useReducer
В этой главе вы узнаете, как использовать React Context + useReducer для управления общими данными между компонентами и страницами, а также реализовать функцию «избранные статьи».
Зачем нам нужно глобальное состояние?
До сих пор данные передаются слой за слоем через Props. Но некоторые данные должны быть доступны нескольким несвязанным компонентам:
- Список избранного должен быть общим для главной страницы, страницы с подробностями и панели навигации
- Все страницы должны знать статус входа пользователя
- Данные корзины должны быть доступны на всех страницах
Передача Props слой за слоем (Prop Drilling)Проблема: A → B → C → D, промежуточные компоненты вынуждены получать Props, которые они не используют.
КонтекстОн предоставляет глобальный «канал данных», через который любой компонент-потомок может читать напрямую, пропуская промежуточные уровни.
Основные концепции контекста
| Шаги | Code | Назначение |
|---|---|---|
| Создание контекста | createContext() | Создайте общий контейнер данных |
| Предоставление данных | <Provider value={...}> | Оберните верхнюю часть дерева компонентов и внедрите данные |
| Использование данных | useContext(MyContext) | Чтение данных в любом компоненте-потомке |
createContext и Provider
Пример
// Шаг 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 вычисляет новое состояние。
Пример
// функция 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.
Пример
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
Пример
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
Пример
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) ? '♥' : '♡'}
</button>
</div>
</div>
</Ссылка>
)
}
export default BlogCard
Кнопка избранного в PostPage
Пример
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) ? '♥ В избранном' : '♡ В избранное'}
</button>
</div>
{/* ... */}
</article>
)
}
Отображение количества избранного в NavBar
Пример
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 + useReducer | Vue3 Pinia |
|---|---|---|
| Способ интеграции | Встроено в React, установка не требуется | Отдельная библиотека, требуется установка |
| Определение состояния | Начальное значение useReducer | state: () => ({}) |
| Обновление состояния | dispatch({ type, payload }) | Прямое изменение или функции действий |
| Вычисляемые свойства | Ручное использование useMemo | геттеры |
| Подход к реактивности | Изменения value в Provider вызывают повторный рендеринг | Автоматическое отслеживание зависимостей |
| DevTools | Нет встроенной поддержки (требуется Redux DevTools) | Нативная поддержка Vue DevTools |
Краткое содержание главы
В этой главе вы освоили полное решение для глобального управления состоянием в React: createContext создает контейнер, Provider внедряет данные, useContext потребляет данные, а useReducer управляет сложной логикой обновления.
Функция «избранные статьи» поддерживает глобальную синхронизацию данных на главной странице, странице с подробной информацией и в панели навигации, и её можно восстановить через localStorage после обновления страницы.
Дополнительные темы