Context API — глобальные данные без лишней передачи
Содержание
- Введение: проблема prop drilling
- Что такое Context API
- Создание и использование Context (базовый пример)
- Context с объектом (несколько значений)
useContextс деструктуризацией- Вложенные Context (несколько контекстов)
- Оптимизация Context (избегаем лишних рендеров)
- Контекст и производительность (React.memo)
- Context vs Redux (когда что использовать)
- Полный пример: приложение с авторизацией
- Context без
useContext(для понимания) - Частые ошибки новичков
- Итог (что мы узнали)
- Задание для закрепления (на 15–20 минут)
- Что дальше?
Цель урока: научиться использовать Context API для передачи данных между компонентами без prop drilling (многоуровневой передачи пропсов). Создавать глобальные состояния для темы, авторизации, языковых настроек и других данных, которые нужны во многих компонентах.
Введение: проблема prop drilling
Prop drilling — это ситуация, когда данные передаются через много уровней компонентов, которые не используют эти данные сами, а просто передают их дальше.
Проблема:
function App() {
const [user, setUser] = useState(null);
return <Header user={user} />; // Передаём в Header
}
function Header({ user }) {
return <Navbar user={user} />; // Просто передаём дальше
}
function Navbar({ user }) {
return <Profile user={user} />; // Опять передаём дальше
}
function Profile({ user }) {
// Только здесь используем!
return <p>{user?.name}</p>;
}
Почему это плохо:
- Много лишнего кода
- Компоненты зависят от данных, которые им не нужны
- Сложно рефакторить и поддерживать
- При добавлении новых данных нужно менять все промежуточные компоненты
Главная мысль: Context решает проблему prop drilling, позволяя передавать данные на любой уровень вложенности без промежуточных компонентов.
Что такое Context API
Из чего состоит Context:
| Компонент | Что делает |
|---|---|
Context |
Создаётся через createContext() |
Provider |
Обёртка, которая предоставляет данные |
Consumer |
Компонент, который потребляет данные (или useContext) |
Главная мысль: Provider делает данные доступными для всех дочерних компонентов. Любой компонент в поддереве может получить эти данные через useContext.
Создание и использование Context (базовый пример)
Шаг 1. Создаём Context
// contexts/ThemeContext.js
import { createContext } from 'react';
// Создаём контекст с начальным значением 'light'
export const ThemeContext = createContext('light');
Шаг 2. Оборачиваем компоненты в Provider
// App.jsx
import { ThemeContext } from './contexts/ThemeContext';
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Toolbar />
</ThemeContext.Provider>
);
}
Шаг 3. Используем данные через useContext
// Toolbar.jsx
import { useContext } from 'react';
import { ThemeContext } from './contexts/ThemeContext';
function Toolbar() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<div style={{
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff',
padding: '20px'
}}>
<p>Текущая тема: {theme}</p>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Переключить тему
</button>
</div>
);
}
Результат: Toolbar получает данные напрямую, без передачи через промежуточные компоненты!
Context с объектом (несколько значений)
Часто в контексте хранят несколько связанных значений:
// contexts/UserContext.js
import { createContext, useState } from 'react';
export const UserContext = createContext(null);
export function UserProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const login = async (email, password) => {
setLoading(true);
// Логика входа
setUser({ email, name: 'Алексей' });
setLoading(false);
};
const logout = () => {
setUser(null);
};
const value = {
user,
loading,
login,
logout,
isAuthenticated: !!user,
};
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}
// Использование в App
function App() {
return (
<UserProvider>
<MainLayout />
</UserProvider>
);
}
Оживи свой сайт. Освой JavaScript!
Статичная верстка — это только скелет. Наш онлайн-курс "JavaScript с нуля до профи" даст твоим страницам мышцы и нервы. Научись создавать слайдеры, формы, интерактивные карты и получать данные с сервера.
От теории — к реальным скриптам в твоём портфолио.
useContext с деструктуризацией
function ProfilePage() {
const { user, loading, logout } = useContext(UserContext);
const { theme } = useContext(ThemeContext);
if (loading) return <p>Загрузка...</p>;
if (!user) return <p>Не авторизован</p>;
return (
<div style={{ background: theme === 'light' ? '#fff' : '#333' }}>
<h2>Профиль</h2>
<p>Имя: {user.name}</p>
<p>Email: {user.email}</p>
<button onClick={logout}>Выйти</button>
</div>
);
}
Вложенные Context (несколько контекстов)
Можно использовать несколько контекстов одновременно:
function App() {
return (
<ThemeProvider>
<UserProvider>
<SettingsProvider>
<Dashboard />
</SettingsProvider>
</UserProvider>
</ThemeProvider>
);
}
function Dashboard() {
const { theme } = useContext(ThemeContext);
const { user } = useContext(UserContext);
const { language } = useContext(SettingsContext);
return (
<div>
<p>Тема: {theme}</p>
<p>Пользователь: {user?.name}</p>
<p>Язык: {language}</p>
</div>
);
}
Оптимизация: Создай кастомный хук для каждого контекста
// hooks/useTheme.js
import { useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// Использование
function Toolbar() {
const { theme, toggleTheme } = useTheme(); // Проще и безопаснее
}
Оптимизация Context (избегаем лишних рендеров)
Проблема: Когда меняется значение контекста, все компоненты, использующие этот контекст, перерендериваются.
Решение 1: разделять контексты
// ❌ Плохо — один контекст для всего
const AppContext = createContext({ user, theme, settings });
// ✅ Хорошо — несколько контекстов
const UserContext = createContext(null);
const ThemeContext = createContext('light');
const SettingsContext = createContext({});
Решение 2: мемоизировать значение
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
// Мемоизируем, чтобы значение не менялось при каждом рендере
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
Контекст и производительность (React.memo)
const Child = React.memo(({ name }) => {
console.log('Child рендерится');
return <p>{name}</p>;
});
function App() {
const [count, setCount] = useState(0);
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<button onClick={() => setCount(count + 1)}>Счёт: {count}</button>
<Child name="Вася" />
</ThemeContext.Provider>
);
}
Важно: даже если Child обёрнут в React.memo, он будет перерендериваться при изменении контекста, потому что контекст — это пропс, который React не может отследить через memo.
Context vs Redux (когда что использовать)
| Характеристика | Context API | Redux |
|---|---|---|
| Сложность | Простой, встроенный | Сложнее, требует установки |
| Производительность | Может быть проблема с частыми обновлениями | Оптимизирован для частых обновлений |
| Размер | Нет внешних зависимостей | Добавляет вес проекту |
| Данные | Хорошо для стабильных данных (тема, язык) | Хорошо для часто меняющихся данных |
| DevTools | Ограниченные | Мощные (Redux DevTools) |
| Когда использовать | Тема, авторизация, настройки | Сложное состояние, много обновлений |
Полный пример: приложение с авторизацией
// contexts/AuthContext.js
import { createContext, useState, useContext } from 'react';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const login = async (email, password) => {
setLoading(true);
try {
// Имитация запроса
await new Promise(resolve => setTimeout(resolve, 1000));
setUser({ email, name: 'Алексей Иванов' });
} catch (error) {
console.error('Ошибка входа:', error);
} finally {
setLoading(false);
}
};
const logout = () => {
setUser(null);
};
return (
<AuthContext.Provider value={{ user, loading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
Использование в компонентах:
function LoginPage() {
const { login, loading } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
login(email, password);
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Пароль"
/>
<button type="submit" disabled={loading}>
{loading ? 'Вход...' : 'Войти'}
</button>
</form>
);
}
function ProfilePage() {
const { user, logout } = useAuth();
if (!user) return <p>Пожалуйста, войдите</p>;
return (
<div>
<h2>Добро пожаловать, {user.name}!</h2>
<p>Email: {user.email}</p>
<button onClick={logout}>Выйти</button>
</div>
);
}
Context без useContext (для понимания)
В старом синтаксисе использовали Consumer:
import { ThemeContext } from './contexts/ThemeContext';
function Toolbar() {
return (
<ThemeContext.Consumer>
{(value) => (
<div style={{ background: value.theme === 'light' ? '#fff' : '#333' }}>
<p>Тема: {value.theme}</p>
</div>
)}
</ThemeContext.Consumer>
);
}
Сейчас используется useContext — он проще и чище.
Частые ошибки новичков
| Ошибка | Почему | Исправление |
|---|---|---|
Забывают обернуть в Provider |
useContext возвращает undefined |
Добавь Provider в родителя |
| Создают контекст для всего | Один контекст на всё приложение | Разделяй по назначению |
| Мутируют объект напрямую | Не вызывает перерендер | Всегда создавай копию |
| Прокидывают функции без мемоизации | Лишние рендеры | Используй useCallback |
| Не проверяют наличие контекста | Ошибки при отсутствии Provider |
Создай кастомный хук с проверкой |
Итог (что мы узнали)
| Что узнали | Как это работает |
|---|---|
| Context API | Глобальные данные без prop drilling |
createContext() |
Создание контекста |
Provider |
Обёртка, предоставляющая данные |
useContext |
Получение данных в компоненте |
| Несколько контекстов | Разные данные в разных контекстах |
| Оптимизация | Разделение, мемоизация |
| Кастомный хук | useAuth, useTheme для удобства |
Задание для закрепления (на 15–20 минут)
Создай систему с тремя контекстами:
ThemeContext— тема приложения (светлая/тёмная)LanguageContext— язык приложения (ru/en)UserContext— авторизация (логин, логаут, проверка)
Создай компоненты:
Header— показывает текущую тему и языкThemeToggle— переключает темуLanguageSelect— переключает язык (dropdown)LoginForm— форма входа (email, password)Profile— показывает данные пользователя, если авторизованRequireAuth— компонент-обёртка, показывающая контент только для авторизованных
Дополнительно: сохраняй тему и язык в localStorage через useEffect.
Проверка: убедись, что все компоненты используют контексты без передачи пропсов.
Что дальше?
В следующем уроке разберём Refs — как работать с DOM-элементами напрямую, управлять фокусом, анимациями, хранить mutable-значения без перерендера и взаимодействовать с сторонними библиотеками. Это важный инструмент для тех случаев, когда React-подход не покрывает все потребности.