Передача пропсов через слои незаинтересованных компонентов — утомительная работа. Сегодня вы выпускаете фичу, а завтра правите шесть файлов только ради того, чтобы переименовать один пропс. В этом и заключается суть prop drilling: у родителя есть данные, глубоко вложенному ребенку они нужны, и каждый компонент между ними превращается в курьера. Приложение продолжает работать, но кодовая база становится хрупкой. Удалите один промежуточный слой — и половина дерева развалится. Измените тип — и TypeScript начнет сыпать ошибками в трех разных директориях. React Context API существует именно для того, чтобы полностью убрать этих посредников.
Как на самом деле выглядит prop drilling
Представьте стандартную оболочку приложения. У вас есть компонент App, который получает данные текущего пользователя. Внутри App живет Layout, внутри Layout находится Sidebar, внутри Sidebar вложен Navigation, и, наконец, внутри Navigation вы находите UserAvatar, которому и нужен объект пользователя.
Ваш код в итоге выглядит так:
function App() {
const user = { name: 'Aarav', role: 'admin' };
return <Layout user={user} />;
}
function Layout({ user }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
return <Navigation user={user} />;
}
function Navigation({ user }) {
return <UserAvatar user={user} />;
}
Layout, Sidebar и Navigation ничего не делают с этим объектом пользователя, кроме как передают его дальше. Они накапливают пропсы, которыми не владеют, их интерфейсы раздуваются, а для их тестирования требуется создание моков данных, которых они даже не касаются. Настоящее преступление — это то, как быстро это распространяется. Добавьте флаг isLoggedIn, строку locale или значение theme — и тот же парад повторится.
Как Context API меняет правила игры
Представьте Context API как Wi-Fi роутер. Вместо того чтобы тянуть длинные кабели в каждую комнату к каждому устройству, роутер передает сигнал по воздуху. Любое устройство в радиусе действия может подключиться напрямую. В терминах React роутер — это Provider, сигнал — это ваше состояние или данные, а устройство — любой вложенный компонент, который вызывает useContext.
Настройка состоит из трех частей:
React.createContext()создает канал передачи данных.- Provider оборачивает часть вашего дерева и передает значение.
- Хук
useContextпозволяет потомкам получать это значение, не затрагивая промежуточные пропсы.
Дерево никуда не девается, но ветвям между корнем и листом больше не нужно заключать «контракт с курьером».
Создание контекста с нуля
Давайте создадим конкретный пример с настройками темы, так как большинству приложений в какой-то момент требуется светлый или темный режим.
Сначала создайте объект контекста. Это «труба»:
import { createContext, useState, useMemo } from 'react';
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export default ThemeContext;
Затем оберните ваше приложение в провайдер. Обычно это делается рядом с корнем:
import { ThemeProvider } from './ThemeContext';
function App() {
return (
<ThemeProvider>
<Layout />
</ThemeProvider>
);
}
Теперь любой потомок может подключиться к сигналу напрямую. Вот кнопка переключения, спрятанная глубоко в интерфейсе:
import { useContext } from 'react';
import ThemeContext from './ThemeContext';
function ThemeToggle() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
onClick={() => setTheme(prev => prev === 'light' ? 'dark' : 'light')}
>
Current theme: {theme}
</button>
);
}
Заметьте, что Layout, Sidebar и Navigation никогда не видят пропс theme. Они рендерятся как обычно, а ThemeToggle берет то, что ему нужно, прямо из контекста. Проводка невидима снаружи, и в этом как раз заключается смысл.
Где на самом деле уместен Context
Context лучше всего подходит для данных, которые разделяются многими удаленными компонентами, но которыми не владеет какой-то один родитель. Хорошие кандидаты:
- Настройки темы, такие как светлый или темный режим, акцентные цвета или масштабирование шрифта.
- Состояние аутентификации, например, объект текущего пользователя, статус входа или срок действия сессии.
- Язык и локаль для интернационализации.
- Данные корзины покупок, которые должны быть синхронизированы между бейджем в хедере, выпадающим списком мини-корзины и страницей оформления заказа.
Не поддавайтесь искушению сваливать каждую часть локального состояния в Context. Поле ввода формы на два уровня ниже не нуждается в глобальной трансляции. Оставьте Context для действительно сквозных задач, а остальное пусть остается обычными пропсами.
Ловушки производительности и как их избежать
Context не бесплатен. Когда значение контекста обновляется, каждый компонент, подключенный к этому контексту, перерендеривается, даже если часть значения, которая его интересует, не изменилась. Классическая ошибка — передавать новый объект-литерал в Provider при каждом рендере родителя.
В нашем примере с темой каждый раз, когда ThemeProvider перерендеривается из-за обновления его собственного родителя, выражение { theme, setTheme } создает совершенно новый объект. React видит новую ссылку, и все потребители обновляются. Если ваша тема меняется редко, а состояние приложения — часто, вы платите за рендеры, которые вам не нужны.
Решение состоит из двух частей.
Разделяйте контексты по частоте обновлений. UserContext, который меняется один раз при входе, не должен использовать тот же провайдер, что и NotificationContext, который обновляется каждые несколько секунд. Держите их раздельно, чтобы статические данные не ехали в том же «поезде ререндеров», что и изменчивые данные.
Оборачивайте значение в useMemo, если значение является объектом или массивом. Дайте React стабильную ссылку:
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
Теперь идентичность объекта меняется только тогда, когда theme действительно изменяется. Дочерние компоненты, которым важен контекст, но которые защищены с помощью React.memo ниже по дереву, пропустят лишнюю работу.
Ошибки, которые тратят ваше время
Две ошибки, которые всё ещё встречаются в продакшн-коде, легко предотвратить.
Во-первых, забывчивость при экспорте самого контекста. Если вы экспортируете только обертку Provider и оставляете объект контекста приватным, разработчик, пишущий новую фичу, не сможет вызвать useContext без рефакторинга вашего модуля. Экспортируйте контекст, чтобы потребители могли чисто импортировать и провайдер, и хук для потребления.
Во-вторых, вызов useContext вне соответствующего Provider. Если ThemeToggle рендерится в ветке дерева, которая не обернута в ThemeProvider, хук вернет значение по умолчанию, переданное в createContext, или undefined, если вы ничего не передали. Это приводит к скрытым ошибкам, таким как cannot read property of undefined. Вы можете защититься от этого, назначив разумное значение по умолчанию или выбрасывая понятную ошибку на раннем этапе вызова хука.
Context против Redux: не усложняйте
Redux нужен не всегда. Для проектов среднего размера Context в сочетании с useState или useReducer покрывает большую часть задач по совместному использованию состояния. Redux раскрывается в полную силу, когда вам нужна отладка с перемещением во времени (time-travel debugging), сложные middleware или глобальные транзакции, которые должны откатываться последовательно. Если всё ваше состояние — это объект пользователя, строка темы и массив корзины, библиотека стора лишь добавит шаблонный код (boilerplate), который вы никогда не используете.
Тем не менее, Context сам по себе не является полноценной системой управления состоянием. Он не дает единого глобального снимка (snapshot) и не объединяет обновления (batching) между несвязанными контекстами. Используйте его как замену prop drilling, а не как операционную систему для всего вашего уровня данных.
Главный вывод
Перестаньте прокидывать пропсы через компоненты, которым они не нужны. Создавайте специализированные контексты для данных, которые действительно охватывают ваше дерево, оборачивайте провайдеры достаточно высоко, чтобы они покрывали потребителей, и всегда стабилизируйте объект значения, когда передаете коллекции или функции. Context API делает ваш React-код прямым: пропсы остаются локальными, глобальные данные передаются «по воздуху», а границы ваших компонентов остаются чистыми.
