Каждый React-разработчик рано или поздно упирается в одну и ту же стену. Вы получаете объект пользователя внутри вашего корневого компонента App. Затем передаете его вниз. И ниже. Через обертку роутера, через оболочку макета, через контейнер боковой панели — и всё это только для того, чтобы крошечный компонент аватара на три уровня глубже мог отобразить фото профиля. Промежуточным компонентам этот объект пользователя не нужен. Они просто пересылают посылку дальше. Это и есть проп-дриллинг, и он превращает чистое дерево компонентов в раздражающую игру в «испорченный телефон».
Настоящая боль начинается, когда меняется структура этих данных. Например, бэкенд начинает возвращать user.profile.avatar вместо user.avatar. Внезапно вам приходится редактировать TypeScript-интерфейсы или PropTypes в пяти файлах, которые сами по себе даже не используют эти данные. Именно здесь на помощь приходит React Context API.
Как Context перестраивает поток данных
Представьте, что Context — это Wi-Fi роутер, расположенный в центре вашего дома. Без него вам пришлось бы прокладывать Ethernet-кабели через каждую комнату, чтобы передать сигнал на ноутбук. С ним же роутер транслирует сигнал по воздуху, и любое устройство с правильным паролем может подключиться напрямую. Стены не имеют значения.
В терминах React корень вашего приложения может транслировать данные через дерево компонентов, не заставляя каждый уровень выступать в роли курьера. Любой вложенный компонент может подписаться на это вещание и получить именно то, что ему нужно.
Три основных элемента
Context API состоит из трех движущихся частей.
React.createContext() создает канал вещания. Он возвращает объект, содержащий Provider и (в старом коде) Consumer. Вам нужно вызвать это только один раз для конкретной функции.
Provider (Провайдер) — это компонент, который оборачивает часть вашего дерева. Он принимает один проп под названием value. Всё, что вы поместите в этот проп, станет доступно каждому потомку, независимо от глубины его вложенности.
useContext — это хук, который позволяет функциональному компоненту подключиться к этому вещанию. Внутри своего компонента вы передаете созданный объект контекста в useContext, и он возвращает текущее значение. Вот и всё. Никаких лишних оберток, никаких дополнительных пропсов.
До появления хуков приходилось использовать паттерн Consumer с render props. Это работало, но создавало много лишней вложенности и нагромождение оберток. useContext упростил всё это до одной строки внутри тела вашей функции.
Когда Context действительно оправдан
Не используйте Context просто по привычке. Он создан для данных, которыми многие несвязанные компоненты делятся в разных ветвях вашего дерева. Хорошие примеры использования:
- Настройки темы. Не только светлая или темная тема, но и токены отступов, цветовые палитры и масштабы шрифтов. Вручную прокидывать их через каждую стилизованную кнопку или модальное окно — быстро надоедает.
- Аутентификация пользователя. Статус входа, массив разрешений или текущий объект пользователя. Ваш хедер, виджет панели управления и защищенный маршрут могут находиться в совершенно разных углах дерева.
- Языковые предпочтения. Строки локали, форматы дат и символы валют. Глубоко вложенным компонентам, таким как метки форм, это необходимо, но не все родительские компоненты на пути должны об этом знать.
- Данные корзины покупок. Количество товаров, общая стоимость и функции добавления в корзину. Бейдж в хедере и страница оформления заказа нуждаются в одном и том же состоянии, но обычно они находятся в совершенно разных ветвях макета.
Практический переключатель темы
Один из самых наглядных способов увидеть Context в действии — это переключатель темы. Вот как это можно реализовать, не упуская важных деталей.
Во-первых, создайте файл ThemeContext.js. Вызовите React.createContext() и сохраните результат. Затем создайте компонент ThemeProvider, который управляет текущей темой с помощью useState или useReducer. Оберните children в Provider вашего контекста, передав объект, содержащий как текущую тему, так и функцию для её переключения. Экспортируйте и ThemeProvider, и сам объект контекста.
Во-вторых, перейдите к точке входа в ваше приложение. Импортируйте ThemeProvider и оберните им всё приложение. Если вы пропустите этот шаг, всё, что попытается прочитать контекст позже, увидит только значение по умолчанию.
В-третьих, внутри компонента Header или Content импортируйте объект контекста и useContext. Вызовите хук, деструктурируйте тему и функцию переключения, и применяйте CSS-классы по условию. Добавьте кнопку, которая вызывает функцию переключения. Компонент никогда не получает проп theme от своего родителя. Он ловит сигнал прямо из воздуха.
Проп-дриллинг, Context или Redux?
Выбор между этими инструментами — это не столько вопрос лояльности, сколько вопрос структуры вашего состояния.
Prop drilling вполне уместен для двух или трех уровней вложенности. Он нагляден, его легко отследить в IDE, и он делает зависимости очевидными. Проблемы возникают только тогда, когда вы начинаете пробрасывать один и тот же проп через шесть или семь слоев.
Context API поставляется вместе с самим React. Это означает отсутствие лишнего веса бандла и необходимости внешней настройки. Он отлично справляется с глобальным состоянием малого и среднего размера, особенно с данными, которые меняются редко, такими как темы или профили пользователей.
Redux требует установки дополнительных библиотек и написания шаблонного кода. Он оправдывает себя, когда логика состояния сложна, когда несколько срезов (slices) состояния глубоко взаимодействуют друг с другом или когда вам нужны отладка с перемещением во времени (time-travel debugging) и middleware. Для простых глобальных данных Redux — это избыточно.
Реальность производительности, о которой никто не говорит
Вот в чем подвох, который отличает решения уровня junior от уровня senior. Когда значение Context Provider меняется, каждый компонент, потребляющий этот контекст, перерисовывается. Неважно, остался ли прежним тот конкретный фрагмент данных, который важен компоненту. React видит новую ссылку и планирует обновление.
Если вы свалите всё состояние приложения в один гигантский StoreContext, вы фактически «склеите» весь свой UI. Изменение настройки темы приведет к перерисовке корзины покупок, графиков на панели управления и списка уведомлений. Это лишняя работа.
Разделяйте контексты по доменам. Используйте ThemeContext для визуальных настроек, UserContext для данных профиля и CartContext для состояния корзины. Если пользователь изменит свое отображаемое имя, ваш хедер обновится, не затрагивая сетку товаров. Также будьте осторожны с тем, что вы передаете в проп value провайдера. Если вы передаете литерал объекта { theme, toggleTheme } прямо во время рендеринга, вы создаете новую ссылку при каждом рендере, что вызывает ненужные обновления. Стабилизируйте эту структуру с помощью useMemo, если значение содержит функции или не примитивные данные.
Ошибки, на которые уходят часы
Две ошибки постоянно ловят команды на крючок.
Забываете экспортировать объект контекста. Легко экспортировать компонент ThemeProvider, а затем попытаться вызвать useContext(ThemeProvider). Это работает не так. Хуку нужен объект контекста, возвращаемый createContext, а не компонент-обертка. Если вы экспортируете только Provider, вашим потребителям (consumers) будет нечего импортировать.
Вызов useContext вне его Provider. Хук возвращает значение по умолчанию, которое вы передали в createContext. Если вы не передали значение по умолчанию, вы получите undefined. Если дерево компонентов рендерит потребителя выше в DOM, чем Provider, или если Provider отсутствует вовсе, ваши данные просто не дойдут. Дважды проверьте, что ваш файл index или root действительно оборачивает приложение.
Главный вывод
React Context — это не революция в управлении состоянием. Это специализированный инструмент для решения конкретной пространственной задачи: доставки данных удаленным компонентам, не превращая каждый слой в почтовое отделение. Используйте его для действительно глобальных данных, разделяйте контексты по доменам для защиты производительности рендеринга и всегда оборачивайте дерево правильным Provider'ом, прежде чем пытаться считать сигнал. Закрепите эти привычки, и ваши деревья компонентов останутся чистыми, быстрыми и понятными.
