Скоупы
Scope — это один запущенный экземпляр состояния.
Модель описывает сторы, события, эффекты и реакции. Scope хранит фактические значения сторов. Благодаря этому одна модель может одновременно работать в приложении, тесте, SSR-запросе, виджете или фоне кешированного экрана.
Scope для чтения стора
Когда код читает count.value, стор должен понять, какую копию count вы имеете в виду. Поэтому прямое чтение и запись требуют scope в текущем контексте выполнения.
scoped(appScope, () => {
count.value += 1;
});scoped(scope, fn) открывает scope, выполняет функцию и восстанавливает предыдущий scope после завершения. Если функция возвращает promise, тот же scope сохраняется для этой promise-цепочки до ее завершения.
Когда scope отсутствует
Чтение или запись стора, а также вызов события или эффекта, когда в текущем контексте нет scope, бросает ошибку Scope is required. Сообщение называет юнит, которому потребовался scope, и перечисляет способы его предоставить:
Scope is required to call event "submitted", but no scope is active.Если сбойный вызов происходит внутри выполняющегося обработчика — например, в реакции, которая потеряла свой scope, — ошибка также печатает цепочку юнитов, которая к ней привела:
Unit path that led here: reaction "applyCheckout" → effect "chargeFx" → event "submitted".По этому пути легко найти, где scope был потерян. Обычная причина — сырой await (голый fetch или таймер) между двумя юнитами внутри тела реакции. Оберните эту работу в effect, который сохраняет scope через свой await.
Один инструмент для запуска и callback-функций
scoped можно использовать сразу:
await scoped(appScope, async () => {
const response = await fetch("/api/count");
count.value = (await response.json()).count;
});А можно создать runner и переиспользовать его там, где иначе код начал бы расползаться:
const inAppScope = scoped(appScope);
await inAppScope(async () => {
count.value += 1;
});
const onMessage = inAppScope.wrap((message: string) => {
messages.items = [...messages.items, message];
});
socket.on("message", onMessage);Так один и тот же объект решает две задачи: выполнить работу прямо сейчас или сохранить callback-функцию, которая позже вернется в тот же scope.
Если код уже выполняется внутри scope, аргумент можно не передавать — scoped() захватывает scope, в котором сейчас выполняется. Это идиоматичный способ привязать колбэк к текущему scope (таймер, слушатель, сокет), не называя scope руками:
scoped(appScope, () => {
// `scoped()` подхватывает активный scope.
scoped(() => {
count.value += 1;
});
// Захватываем текущий scope для последующего колбэка.
const onMessage = scoped().wrap((message: string) => {
messages.items = [...messages.items, message];
});
socket.on("message", onMessage);
});Когда у колбэка не оказывается scope — потому что его отдали таймеру, слушателю, сокету или сырому await, — это потеря scope. Обёртка через scoped() — это решение.
Заполнение значений scope
Свежий scope стартует каждый стор с его начального значения. Чтобы запустить scope с конкретными значениями — это типично для тестов, SSR-гидрации или предпросмотров — передайте их при создании scope:
const appScope = scope({
values: [
[count, 10],
[query, "docs"],
],
});Чтобы выставить значение в уже существующем scope императивно, используйте seedScopeStoreValue. Заполнять можно только writable-сторы; derived- и read-only-сторы бросят ошибку.
seedScopeStoreValue(appScope, count, 10);Чтение текущего scope
getCurrentScope() возвращает scope, активный в текущем контексте выполнения, или null, если ни один scope не открыт. Тянитесь к нему, только когда действительно нужен сам объект scope. Чтобы захватить текущий scope и переоткрыть его в последующем callback, предпочитайте scoped() — он сам захватывает активный scope, так что scope не нужно передавать руками.
const current = getCurrentScope();Запуск юнитов на границах
scoped удобен для обычного кода. Он же подходит и когда вы запускаете юнит на границе системы — из теста, серверного загрузчика, команды или адаптера фреймворка: оберните вызов в scoped и await-ните возвращённый промис.
await scoped(appScope, () => incremented(1));scoped явно показывает, какой scope владеет состоянием, а callback — какой юнит стартует и какой payload входит в граф. Его промис также ждет завершения асинхронной работы, поднятой этим запуском.
Правила scope
Scope живёт в текущем контексте выполнения, поэтому главное — не потерять его через await. Правила:
- Чтение и запись требуют scope.
store.value,event(),effect()иdependency.valueразрешаются относительно активного scope. Если ни один не активен — бросаетсяScope is required. - Scope активен внутри блока
scoped(scope, …), обработчика эффекта и тела реакции — и любого синхронного кода, который они вызывают. Юнит выполняется в том scope, в котором сработал его источник. - Ожидание юнита сохраняет scope. После
await someFx()илиawait someEvent()продолжение выполняется в том же scope, поэтому сразу после можно читать сторы. Эффекты и события восстанавливают scope, который был активен в момент их вызова. - Сырой
awaitтеряет scope.await fetch(),await delay(),await anyPromise()— у продолжения нет scope, и следующее чтение стора бросит ошибку. Оборачивайте внешнюю асинхронность вeffect, чтобы scope пережил границу. - На границах системы называйте scope явно. Из теста, загрузчика, команды или callback-функции, отданной другой библиотеке, используйте
scoped(scope, fn)илиscoped(scope).wrap(cb), а не рассчитывайте на ambient scope.
Правило большого пальца: внутри реакции или эффекта await-ьте только юниты — await someFx(), await someEvent(), await scoped(...). Как только вы сделали await голого промиса — вы вышли из scope; перенесите эту работу в effect.
О конкретных способах потерять scope — таймеры, слушатели, сокеты, сырые await — и как чинить каждый, см. Потеря scope.