Хотите заказать веб-сайт? Связаться с нами

Context API — глобальные данные без лишней передачи

Цель урока: научиться использовать Context API для передачи данных между компонентами без prop drilling (многоуровневой передачи пропсов). Создавать глобальные состояния для темы, авторизации, языковых настроек и других данных, которые нужны во многих компонентах.

Context API — глобальные данные без лишней передачи

Введение: проблема 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>;
}

Почему это плохо:

  1. Много лишнего кода
  2. Компоненты зависят от данных, которые им не нужны
  3. Сложно рефакторить и поддерживать
  4. При добавлении новых данных нужно менять все промежуточные компоненты

Главная мысль: Context решает проблему prop drilling, позволяя передавать данные на любой уровень вложенности без промежуточных компонентов.

Что такое Context API

Context — это механизм React для передачи данных через дерево компонентов без передачи пропсов на каждом уровне.

Из чего состоит 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 минут)

Создай систему с тремя контекстами:

  1. ThemeContext — тема приложения (светлая/тёмная)
  2. LanguageContext — язык приложения (ru/en)
  3. UserContext — авторизация (логин, логаут, проверка)

Создай компоненты:

  1. Header — показывает текущую тему и язык
  2. ThemeToggle — переключает тему
  3. LanguageSelect — переключает язык (dropdown)
  4. LoginForm — форма входа (email, password)
  5. Profile — показывает данные пользователя, если авторизован
  6. RequireAuth — компонент-обёртка, показывающая контент только для авторизованных

Дополнительно: сохраняй тему и язык в localStorage через useEffect.

Проверка: убедись, что все компоненты используют контексты без передачи пропсов.

Что дальше?

В следующем уроке разберём Refs — как работать с DOM-элементами напрямую, управлять фокусом, анимациями, хранить mutable-значения без перерендера и взаимодействовать с сторонними библиотеками. Это важный инструмент для тех случаев, когда React-подход не покрывает все потребности.

Теги: