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 — Чтобы понять, почему все потребители ре-рендерятся при смене значения контекста, нужна модель рендера
useContext: данные сквозь дерево

0

1

Войти