React
useContext: данные сквозь дерево
Тема оформления - тёмная или светлая - нужна кнопке в глубине дерева, в десятке уровней от того места, где она хранится. Без специального механизма пришлось бы передавать theme как prop через каждый промежуточный компонент, который сам её не использует, а только пробрасывает дальше. Десять лишних props в десяти компонентах ради одного значения внизу. Эта боль называется prop drilling, и контекст придуман именно против неё.
- Тема оформления: dark/light нужна множеству компонентов на всех уровнях, и переключатель меняет её для всех сразу
- Текущий пользователь: имя, аватар и права доступа читают хедер, сайдбар, формы и кнопки в разных частях дерева
- Локализация: выбранный язык и функция перевода t() доступны любому компоненту без проброса через props
- UI-библиотеки: Material UI, Chakra и Radix раздают свою тему и настройки через контекст под капотом
- Роутер и формы: react-router и react-hook-form передают текущий маршрут и состояние формы вложенным компонентам через контекст
Предварительные знания
- props: передача данных от родителя к ребёнку и однонаправленный поток данных
- useState и подъём состояния наверх (lifting state up) к общему родителю
- Понимание дерева компонентов: родители, дети и вложенность
Боль prop drilling
Данные в React текут сверху вниз через props. Пока дерево неглубокое, это удобно и предсказуемо. Но если значение нужно компоненту глубоко внизу, а хранится оно высоко наверху, его приходится передавать через каждый промежуточный уровень. Эти промежуточные компоненты сами значение не используют - они только принимают его как prop и пробрасывают дальше. Такой сквозной проброс через цепочку безразличных компонентов называют prop drilling.
Проблема не в одном prop, а в масштабе. Когда таких сквозных значений несколько (тема, пользователь, язык), а дерево глубокое, промежуточные компоненты обрастают props, которые им не нужны. Любое добавление нового значения требует править всю цепочку. Код становится хрупким: компоненты знают о данных, которые лишь транзитом проходят сквозь них.
Prop drilling сам по себе не зло - на одном-двух уровнях явная передача props читается лучше любой магии. Проблемой он становится, когда цепочка длинная, а значение по-настоящему глобальное и нужно во многих местах. Именно для таких случаев придуман контекст.
В чём суть проблемы prop drilling?
createContext, Provider и useContext
Контекст состоит из трёх частей. createContext создаёт объект контекста со значением по умолчанию. Компонент Provider оборачивает поддерево и задаёт значение через prop value - это значение становится доступным всем потомкам внутри. Хук useContext читает текущее значение в любом компоненте поддерева, минуя все промежуточные уровни. Промежуточные компоненты больше ничего не знают про эти данные.
В React 19 сам объект контекста можно использовать как Provider: ThemeContext value={...} вместо прежнего ThemeContext.Provider value={...}. Старая форма с .Provider продолжает работать, но новая короче и считается предпочтительной.
Важно понимать: контекст не хранит состояние сам по себе. Он лишь канал для передачи значения. Само изменяемое состояние по-прежнему живёт в useState или useReducer у компонента-владельца (здесь это App), а контекст только доставляет его потомкам. Значение по умолчанию из createContext используется, только если компонент читает контекст без обёртки в Provider.
Где на самом деле хранится изменяемое состояние при использовании контекста для темы?
Ре-рендер потребителей и границы контекста
У контекста есть важное свойство: когда значение в Provider меняется, React перерисовывает все компоненты, которые читают этот контекст через useContext, независимо от того, как глубоко они сидят. Это ровно то, что нужно для редко меняющихся данных вроде темы: переключили - обновилось везде. Но если значение меняется часто, а потребителей много, лишние ре-рендеры могут стать заметными.
Частая ошибка: передавать в value свежесозданный объект на каждом рендере, например value={{ user, setUser }}. Новый объект - новая ссылка, и все потребители перерисовываются при каждом рендере владельца, даже если данные не менялись. Если объект собирается на месте, его стоит стабилизировать или разнести на отдельные контексты.
| Инструмент | Когда уместен | Что решает |
|---|---|---|
| useContext | Редко меняющиеся глобальные данные: тема, пользователь, язык | Доставка значения сквозь дерево без prop drilling |
| Zustand | Частое клиентское состояние, точечные подписки | Обновления без ре-рендера всего поддерева |
| TanStack Query | Серверные данные: кеш, рефетч, синхронизация | Запросы, кеширование, гонки и повторные попытки |
| Redux Toolkit | Крупное предсказуемое состояние, devtools, middleware | Централизованный стор с историей действий |
Контекст - не замена менеджеру состояния, а механизм его доставки. Для частого клиентского состояния с тонким контролем над ре-рендерами берут Zustand: компонент подписывается только на нужный кусок стора. Для серверных данных - TanStack Query, которая снимает с разработчика кеш, рефетч и гонки запросов. Контекст же остаётся идеальным для глобальных значений, которые меняются редко.
Полезный приём против лишних ре-рендеров - разделять контексты по частоте изменений. Например, держать тему и текущего пользователя в разных контекстах: смена пользователя не должна перерисовывать тех, кому нужна только тема.
Что произойдёт с компонентами, которые читают контекст через useContext, когда значение в Provider изменится?
Связь с другими темами
Урок про передачу данных сквозь дерево. Рядом и дальше:
- useState — Контекст обычно раздаёт значение, хранящееся в useState у верхнего компонента
- useReducer — Сложное состояние удобно держать в редьюсере и раздавать state и dispatch через контекст
- Модель рендера — Объясняет, почему смена значения контекста перерисовывает всех его потребителей
Итог
- Prop drilling - это проброс props через промежуточные компоненты, которым данные не нужны, только чтобы доставить их вниз
- createContext создаёт объект контекста, Provider задаёт значение для поддерева, useContext читает его в любом потомке
- Контекст не хранит состояние сам - он лишь раздаёт значение, которое обычно живёт в useState или useReducer выше по дереву
- При смене значения в Provider ре-рендерятся все компоненты, которые читают этот контекст через useContext
- Контекст хорош для редко меняющихся глобальных данных (тема, пользователь, язык); для частых обновлений и сложного клиентского состояния берут Zustand, Redux или серверные данные через TanStack Query
Связанные уроки
- rc-06-usestate — Контекст обычно раздаёт значение из useState, поднятого наверх дерева, поэтому без состояния он бессмыслен
- rc-15-usereducer — Связка useReducer + контекст - типичный способ раздавать сложное состояние и dispatch по всему дереву
- rc-10-render-mental-model — Чтобы понять, почему все потребители ре-рендерятся при смене значения контекста, нужна модель рендера