Svelte

Context API и руны

Тема оформления (светлая или тёмная) нужна и шапке, и сайдбару, и кнопке в глубине формы на пятом уровне вложенности. Без контекста значение темы пришлось бы пробросить пропсом через каждый промежуточный компонент, даже если сам он темой не пользуется. Это называется prop drilling, и в крупном дереве он засоряет сигнатуры десятков компонентов. Context API SvelteKit позволяет родителю положить тему в контекст один раз, а любому потомку достать её напрямую.

  • Темы оформления: провайдер темы кладёт реактивный объект в контекст, и компоненты подхватывают цвета без пропсов
  • Локализация: текущий язык и функция перевода доступны любому компоненту дерева через контекст
  • Compound-компоненты: Tabs делится активной вкладкой с вложенными Tab без явной передачи через каждый уровень
  • Формы: контекст формы хранит значения и ошибки полей, к которым обращаются вложенные инпуты

Предварительные знания

  • Понимание реактивного состояния через руну `$state`
  • Знание того, что такое пропсы и как данные передаются от родителя к ребёнку
  • Базовое понимание дерева компонентов и вложенности

setContext и getContext: значение вниз по дереву

Контекст в Svelte это пара функций. setContext(key, value), вызванная в компоненте, делает value доступным всем его потомкам. getContext(key), вызванная в любом потомке, возвращает это значение. Ключ может быть строкой или, надёжнее, символом. Важное ограничение: обе функции вызываются при инициализации компонента, то есть в теле script верхнего уровня, а не внутри обработчиков или эффектов.

Между родителем и потомком может быть сколько угодно промежуточных компонентов, и ни одному из них не нужно знать про lang. В этом и состоит устранение prop drilling: значение не течёт через цепочку пропсов, а доступно напрямую из контекста. Контекст виден только потомкам того компонента, который его установил, а не всему приложению.

getContext работает только если вызван при инициализации компонента, который является потомком установившего контекст. Вызов getContext из обработчика клика, асинхронного коллбэка или компонента вне поддерева вернёт undefined. Контекст это про структуру дерева, а не про глобальный доступ.

Компонент A устанавливает setContext('theme', ...). Компонент B это потомок A через три промежуточных уровня, ни один из которых тему не пробрасывает. Может ли B получить тему через getContext('theme')?

Реактивное состояние через контекст

Сам по себе контекст не реактивен: setContext передаёт значение по ссылке один раз. Чтобы потомки видели изменения, в контекст кладут не примитив, а объект, чьи поля являются реактивным состоянием через `$state`. Тогда чтение поля у потомка подписывается на сигнал, и изменение поля у владельца обновляет потомка. Ключ в том, чтобы передавать сам реактивный объект, а не снимок его значения.

Когда родитель меняет theme.mode, потомок, читающий theme.mode, перерисовывается точечно. Это работает, потому что в контекст положен один и тот же реактивный объект, и обе стороны обращаются к его полям. Если бы вместо объекта передали setContext('theme', theme.mode), потомок получил бы строку-снимок, и последующие изменения до него не дошли бы.

Чтобы спрятать ключ и дать типобезопасность, контекст оборачивают в пару функций в отдельном модуле: setThemeContext и getThemeContext с общим символом-ключом. Компоненты импортируют хелперы и не работают со строковыми ключами напрямую, что исключает опечатки и рассинхрон типов.

Разработчик кладёт в контекст результат разворачивания состояния: setContext('count', state.value), где state создан через `$state`. Почему потомок не увидит последующих изменений count?

Почему контекст безопасен относительно запросов

На сервере один процесс обрабатывает запросы многих пользователей одновременно. Состояние, хранимое в переменной на уровне модуля, оказывается общим для всех этих запросов, и данные одного пользователя могут утечь к другому. Контекст лишён этой проблемы по своей природе: он создаётся заново при каждой инициализации дерева компонентов, а каждый запрос рендерит собственное дерево.

Когда SvelteKit рендерит страницу для запроса, он строит свежий экземпляр дерева компонентов. setContext в этом дереве кладёт значение, видимое только в нём. Параллельный запрос другого пользователя получает отдельное дерево с отдельным контекстом. Состояние не пересекается, потому что оно привязано к экземпляру дерева, а не к модулю, общему для всего процесса.

  • Состояние на уровне модуля — Одна переменная на весь серверный процесс. Общая для всех одновременных запросов, поэтому данные пользователей могут смешаться. Опасно для пользовательских данных на сервере
  • Состояние через контекст — Создаётся заново для каждого дерева компонентов, а значит для каждого запроса. Изолировано по запросам естественным образом. Безопасно для пользовательских данных

Отсюда практическое правило: пользовательское состояние, которое должно быть своим у каждого запроса (текущий пользователь, корзина, сессия), на сервере держат в контексте, а не в переменной модуля. Контекст это рекомендованный способ разделять состояние по дереву без риска утечки между запросами.

Это не значит, что модульное состояние запрещено всегда. Для значений, действительно одинаковых для всех (например, неизменяемая конфигурация), модуль уместен. Опасны именно изменяемые пользовательские данные на уровне модуля на сервере, и контекст их этой опасности лишает.

Почему хранение текущего пользователя в контексте безопаснее на сервере, чем в изменяемой переменной на уровне модуля?

Связь с другими темами

Контекст это способ делиться состоянием по дереву. Он соседствует с модульным состоянием и лежит в основе паттернов компонентов:

  • Реактивное состояние — В контекст кладут именно `$state`, чтобы он оставался реактивным у потомков
  • Глобальное состояние в модулях — Альтернатива контексту, но без автоматической изоляции по запросам на сервере
  • Паттерны компонентов — Compound-компоненты строятся на контексте: родитель делится состоянием с потомками

Итог

  • setContext(key, value) кладёт значение в контекст текущего компонента, а getContext(key) достаёт его в любом потомке
  • Контекст устраняет prop drilling: значение не нужно пробрасывать через промежуточные компоненты, которые им не пользуются
  • Чтобы состояние осталось реактивным, в контекст кладут объект с `$state`, а не разворачивают его в момент чтения
  • Контекст привязан к дереву компонентов и устанавливается при инициализации, поэтому он изолирован между запросами на сервере
  • Удобный приём: оборачивать setContext и getContext в пару функций-хелперов с типизированным ключом

Связанные уроки

  • sv-06-state — Через контекст передаётся именно реактивное состояние из `$state`, поэтому работа с состоянием это база
  • sv-33-shared-state-modules — Контекст и модульное состояние решают похожую задачу общего состояния, но с разными гарантиями изоляции по запросам
  • sv-35-component-patterns — Контекст лежит в основе compound-компонентов, где родитель делится состоянием с потомками без пропсов
Context API и руны

0

1

Войти