Расскажите про хук - useContext, как он работает, где применяется и для чего? Когда лучше использовать, а когда лучше отказаться?

👨‍💻 Frontend Developer 🟠 Может встретиться 🎚️ Средний
#React #Hooks

Краткий ответ

useContext — это хук в React, который позволяет получать значения из React Context! Он предоставляет способ передачи данных глубоко вниз по дереву компонентов без пропс-дриллинга.

Ключевые особенности:

  • Упрощает доступ к контексту по сравнению с Consumer
  • Перерисовывает компонент при изменении значения контекста
  • Не заменяет Redux или другие системы управления состоянием
  • Работает только с контекстами, созданными через React.createContext()

Пример использования:

import React, { createContext, useContext } from 'react';
 
const ThemeContext = createContext('light');
 
function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}
 
function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}
 
function ThemedButton() {
  const theme = useContext(ThemeContext);
  
  return (
    <button className={theme}>
      Кнопка с темой {theme}
    </button>
  );
}

Полный ответ

Хук useContext — это один из встроенных хуков React, который предоставляет способ подписки на контекст и получения его значений в функциональных компонентах. Это современная альтернатива старому API с Consumer, которая делает код более читаемым и лаконичным. 🌟

Как работает useContext?

useContext принимает объект контекста (значение, возвращаемое из React.createContext) и возвращает текущее значение контекста для этого контекста:

const value = useContext(MyContext);

Компонент, вызывающий useContext, всегда будет перерисовываться при изменении значения контекста. React берет текущее значение контекста из ближайшего соответствующего Provider над компонентом в дереве.

Создание и использование контекста

import React, { createContext, useContext } from 'react';
 
// 1. Создаем контекст с начальным значением
const UserContext = createContext({
  name: 'Гость',
  isLoggedIn: false
});
 
// 2. Компонент-провайдер
function UserProvider({ children }) {
  const [user, setUser] = React.useState({
    name: 'Александр',
    isLoggedIn: true
  });
  
  return (
    <UserContext.Provider value={user}>
      {children}
    </UserContext.Provider>
  );
}
 
// 3. Компонент, использующий контекст
function UserProfile() {
  const user = useContext(UserContext);
  
  return (
    <div>
      <h1>Привет, {user.name}!</h1>
      <p>Статус: {user.isLoggedIn ? 'Авторизован' : 'Гость'}</p>
    </div>
  );
}
 
// 4. Использование в приложении
function App() {
  return (
    <UserProvider>
      <UserProfile />
    </UserProvider>
  );
}

Основные применения useContext

1. Темизация приложения

import React, { createContext, useContext, useState } from 'react';
 
const ThemeContext = createContext();
 
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };
  
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}
 
function ThemedButton() {
  const { theme, toggleTheme } = useContext(ThemeContext);
  
  return (
    <button 
      className={`btn btn-${theme}`}
      onClick={toggleTheme}
    >
      Переключить на {theme === 'light' ? 'темную' : 'светлую'} тему
    </button>
  );
}
 
function App() {
  return (
    <ThemeProvider>
      <div className="app">
        <h1>Приложение с темизацией</h1>
        <ThemedButton />
      </div>
    </ThemeProvider>
  );
}

2. Глобальное состояние пользователя

import React, { createContext, useContext, useReducer } from 'react';
 
const AuthContext = createContext();
 
const authReducer = (state, action) => {
  switch (action.type) {
    case 'LOGIN':
      return {
        user: action.payload,
        isAuthenticated: true
      };
    case 'LOGOUT':
      return {
        user: null,
        isAuthenticated: false
      };
    default:
      return state;
  }
};
 
function AuthProvider({ children }) {
  const [state, dispatch] = useReducer(authReducer, {
    user: null,
    isAuthenticated: false
  });
  
  const login = (userData) => {
    dispatch({ type: 'LOGIN', payload: userData });
  };
  
  const logout = () => {
    dispatch({ type: 'LOGOUT' });
  };
  
  return (
    <AuthContext.Provider value={{ ...state, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}
 
function UserProfile() {
  const { user, isAuthenticated, logout } = useContext(AuthContext);
  
  if (!isAuthenticated) {
    return <div>Пожалуйста, войдите в систему</div>;
  }
  
  return (
    <div>
      <h2>Привет, {user.name}!</h2>
      <button onClick={logout}>Выйти</button>
    </div>
  );
}

3. Локализация/переводы

import React, { createContext, useContext } from 'react';
 
const translations = {
  en: {
    greeting: 'Hello',
    welcome: 'Welcome to our app'
  },
  ru: {
    greeting: 'Привет',
    welcome: 'Добро пожаловать в наше приложение'
  }
};
 
const LanguageContext = createContext();
 
function LanguageProvider({ children, defaultLanguage = 'en' }) {
  const [language, setLanguage] = React.useState(defaultLanguage);
  
  const t = (key) => {
    return translations[language]?.[key] || key;
  };
  
  return (
    <LanguageContext.Provider value={{ language, setLanguage, t }}>
      {children}
    </LanguageContext.Provider>
  );
}
 
function Greeting() {
  const { t, language, setLanguage } = useContext(LanguageContext);
  
  return (
    <div>
      <h1>{t('greeting')}!</h1>
      <p>{t('welcome')}</p>
      <select 
        value={language} 
        onChange={(e) => setLanguage(e.target.value)}
      >
        <option value="en">English</option>
        <option value="ru">Русский</option>
      </select>
    </div>
  );
}

Преимущества useContext

1. Устранение пропс-дриллинга

// ❌ Без контекста - пропс-дриллинга
function App() {
  const theme = 'dark';
  return <Layout theme={theme} />;
}
 
function Layout({ theme }) {
  return <Header theme={theme} />;
}
 
function Header({ theme }) {
  return <Button theme={theme} />;
}
 
function Button({ theme }) {
  return <button className={`btn-${theme}`}>Кнопка</button>;
}
 
// ✅ С контекстом - прямой доступ
const ThemeContext = createContext('light');
 
function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Layout />
    </ThemeContext.Provider>
  );
}
 
function Layout() {
  return <Header />;
}
 
function Header() {
  return <Button />;
}
 
function Button() {
  const theme = useContext(ThemeContext);
  return <button className={`btn-${theme}`}>Кнопка</button>;
}

2. Упрощение API компонентов

// ❌ Сложный API с множеством пропсов
function ComplexComponent({ 
  theme, 
  locale, 
  permissions, 
  userSettings,
  // ... еще 10 пропсов
}) {
  // ...
}
 
// ✅ Чистый API с контекстом
function CleanComponent() {
  // Получаем все нужные данные из контекстов
  const theme = useContext(ThemeContext);
  const locale = useContext(LocaleContext);
  const permissions = useContext(PermissionsContext);
  const userSettings = useContext(UserSettingsContext);
  
  // ...
}

Когда лучше использовать useContext

Используйте useContext когда:

  1. Глубокая передача данных — когда нужно передать данные на много уровней вниз по дереву компонентов
  2. Глобальное состояние — темы, пользовательские данные, настройки приложения
  3. Состояние между сестринскими компонентами — когда несколько компонентов нуждаются в одинаковых данных
  4. Простое приложение — для небольших приложений без сложной логики управления состоянием
// Хорошее применение useContext
const AppSettingsContext = createContext();
 
function App() {
  return (
    <AppSettingsContext.Provider value={{
      theme: 'dark',
      language: 'ru',
      notifications: true
    }}>
      <MainLayout />
    </AppSettingsContext.Provider>
  );
}
 
function NotificationPanel() {
  const { notifications } = useContext(AppSettingsContext);
  
  if (!notifications) return null;
  
  return <div>Панель уведомлений</div>;
}

Когда лучше отказаться от useContext

Избегайте useContext когда:

  1. Сложное управление состоянием — для приложений со сложной логикой, множественными обновлениями состояния лучше использовать Redux, Zustand или другие библиотеки
  2. Частые обновления — если контекст часто обновляется, это может вызвать ненужные перерисовки многих компонентов
  3. Только для передачи функций — если вы только передаете функции через контекст, возможно стоит рассмотреть другие подходы
  4. Простая иерархия — если данные передаются максимум на 2-3 уровня вниз, пропсы могут быть проще
// Плохое применение useContext
const ExpensiveDataContext = createContext();
 
// ❌ Контекст обновляется слишком часто
function DataProvider({ children }) {
  const [data, setData] = useState([]);
  
  // Обновление данных каждую секунду
  useEffect(() => {
    const interval = setInterval(() => {
      setData(prev => [...prev, Date.now()]);
    }, 1000);
    
    return () => clearInterval(interval);
  }, []);
  
  return (
    <ExpensiveDataContext.Provider value={data}>
      {children}
    </ExpensiveDataContext.Provider>
  );
}
 
// Все компоненты будут перерисовываться каждую секунду
function ExpensiveComponent() {
  const data = useContext(ExpensiveDataContext);
  // Дорогостоящая операция с данными
  return <div>{JSON.stringify(data)}</div>;
}

Оптимизация useContext

1. Разделение контекстов

// ❌ Один большой контекст
const AppContext = createContext();
 
function AppProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState(null);
  const [notifications, setNotifications] = useState([]);
  
  return (
    <AppContext.Provider value={{
      theme, setTheme,
      user, setUser,
      notifications, setNotifications
    }}>
      {children}
    </AppContext.Provider>
  );
}
 
// ✅ Разделенные контексты
const ThemeContext = createContext();
const UserContext = createContext();
const NotificationsContext = createContext();
 
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}
 
function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  return (
    <UserContext.Provider value={{ user, setUser }}>
      {children}
    </UserContext.Provider>
  );
}
 
// Компоненты перерисовываются только при изменении своих данных
function ThemeToggle() {
  const { theme, setTheme } = useContext(ThemeContext);
  // Перерисовывается только при изменении темы
}
 
function UserProfile() {
  const { user } = useContext(UserContext);
  // Перерисовывается только при изменении пользователя
}

2. Мемоизация значений контекста

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const [accentColor, setAccentColor] = useState('blue');
  
  // ❌ Новое значение объекта при каждом рендере
  const contextValue = {
    theme,
    setTheme,
    accentColor,
    setAccentColor
  };
  
  // ✅ Мемоизация значения контекста
  const contextValue = React.useMemo(() => ({
    theme,
    setTheme,
    accentColor,
    setAccentColor
  }), [theme, setTheme, accentColor, setAccentColor]);
  
  return (
    <ThemeContext.Provider value={contextValue}>
      {children}
    </ThemeContext.Provider>
  );
}

3. Создание пользовательских хуков

// Пользовательский хук для работы с контекстом
function useTheme() {
  const context = useContext(ThemeContext);
  
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  
  return context;
}
 
// Использование
function ThemedButton() {
  const { theme, toggleTheme } = useTheme();
  
  return (
    <button 
      className={`btn-${theme}`}
      onClick={toggleTheme}
    >
      Переключить тему
    </button>
  );
}

Распространенные ошибки

1. Использование useContext без Provider

// ❌ Ошибка: использование контекста без провайдера
const ThemeContext = createContext('light');
 
function Button() {
  const theme = useContext(ThemeContext); // Будет 'light' (значение по умолчанию)
  return <button className={theme}>Кнопка</button>;
}
 
// ✅ Правильно: использование с провайдером
function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Button />
    </ThemeContext.Provider>
  );
}

2. Изменение объекта контекста напрямую

// ❌ Ошибка: изменение объекта контекста напрямую
const UserContext = createContext();
 
function UserProvider({ children }) {
  const [user, setUser] = useState({ name: 'Иван', age: 30 });
  
  // Неправильное изменение объекта
  const updateUser = (field, value) => {
    user[field] = value; // Мутирует состояние напрямую
    setUser(user); // Не вызовет перерисовку
  };
  
  return (
    <UserContext.Provider value={{ user, updateUser }}>
      {children}
    </UserContext.Provider>
  );
}
 
// ✅ Правильно: создание нового объекта
function UserProvider({ children }) {
  const [user, setUser] = useState({ name: 'Иван', age: 30 });
  
  const updateUser = (field, value) => {
    setUser(prev => ({ ...prev, [field]: value })); // Создает новый объект
  };
  
  return (
    <UserContext.Provider value={{ user, updateUser }}>
      {children}
    </UserContext.Provider>
  );
}

3. Создание контекста внутри компонента

// ❌ Ошибка: создание контекста внутри компонента
function App() {
  // Контекст создается при каждом рендере
  const ThemeContext = createContext('light');
  
  return (
    <ThemeContext.Provider value="dark">
      <Button />
    </ThemeContext.Provider>
  );
}
 
// ✅ Правильно: создание контекста на верхнем уровне
const ThemeContext = createContext('light');
 
function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Button />
    </ThemeContext.Provider>
  );
}

4. Использование useContext в условных выражениях

// ❌ Ошибка: использование хука в условии
function Component() {
  if (someCondition) {
    const value = useContext(MyContext); // Ошибка правил хуков!
    // ...
  }
  
  // ...
}
 
// ✅ Правильно: всегда вызывайте хуки на верхнем уровне
function Component() {
  const value = useContext(MyContext); // Правильно
  
  if (someCondition) {
    // Используйте value внутри условия
    // ...
  }
  
  // ...
}

Сравнение с другими подходами

useContext vs Redux

КритерийuseContextRedux
СложностьПростаяСложная
Размер бандла0 (встроенный)~2KB
ОтладкаОграниченнаяРасширенная (Redux DevTools)
МасштабируемостьОграниченнаяВысокая
Кривая обученияНизкаяВысокая
Подходит дляНебольших приложенийСложных приложений
// useContext для простого приложения
const AppStateContext = createContext();
 
function AppStateProvider({ children }) {
  const [state, setState] = useState({
    user: null,
    theme: 'light',
    notifications: []
  });
  
  return (
    <AppStateContext.Provider value={[state, setState]}>
      {children}
    </AppStateContext.Provider>
  );
}
 
// Redux для сложного приложения
import { createStore } from 'redux';
import { useSelector, useDispatch } from 'react-redux';
 
const store = createStore(rootReducer);
 
function UserProfile() {
  const user = useSelector(state => state.user);
  const dispatch = useDispatch();
  
  // ...
}

useContext vs Пропсы

КритерийuseContextПропсы
ЧитаемостьВысокая (меньше кода)Средняя (много пропсов)
ЯвностьНизкая (зависимости скрыты)Высокая (все явно передается)
ГибкостьОграниченнаяВысокая
ПроизводительностьСредняяВысокая
ПоддержкаСредняяВысокая
// Пропсы - явная передача данных
function Parent() {
  const theme = 'dark';
  return <Child theme={theme} />;
}
 
function Child({ theme }) {
  return <GrandChild theme={theme} />;
}
 
function GrandChild({ theme }) {
  return <button className={theme}>Кнопка</button>;
}
 
// useContext - неявная передача данных
const ThemeContext = createContext('light');
 
function Parent() {
  return (
    <ThemeContext.Provider value="dark">
      <Child />
    </ThemeContext.Provider>
  );
}
 
function Child() {
  return <GrandChild />;
}
 
function GrandChild() {
  const theme = useContext(ThemeContext);
  return <button className={theme}>Кнопка</button>;
}

Резюме

useContext — это хук React для:

  • Доступа к значениям контекста в функциональных компонентах
  • Устранения пропс-дриллинга при передаче данных глубоко вниз по дереву
  • Упрощения API компонентов за счет удаления промежуточных пропсов

Когда использовать:

  • Глубокая передача данных (3+ уровней)
  • Глобальное состояние (темы, пользовательские данные)
  • Состояние между сестринскими компонентами
  • Простые приложения без сложной логики управления состоянием

Когда избегать:

  • Сложное управление состоянием (лучше Redux/MobX)
  • Частые обновления контекста (может вызвать проблемы с производительностью)
  • Когда данные передаются на 1-2 уровня вниз (пропсы проще)
  • Только для передачи функций (рассмотрите другие подходы)

Лучшие практики:

  • Разделяйте контексты по типам данных
  • Используйте useMemo для мемоизации значений контекста
  • Создавайте пользовательские хуки для работы с контекстами
  • Проверяйте наличие провайдера в пользовательских хуках
  • Не создавайте контексты внутри компонентов

useContext — мощный инструмент для управления состоянием в React-приложениях, который помогает избежать пропс-дриллинга и делает код более читаемым. Однако важно понимать, когда его использовать, а когда предпочесть более специализированные решения. 🚀


Хотите больше статей для подготовки к собеседованиям? Подписывайтесь на EasyAdvice, добавляйте сайт в закладки и совершенствуйтесь каждый день 💪