import { type Scope } from "effector";

Scope - это полностью изолированный экземпляр приложения. Основное назначение Scope включает SSR (Server-Side Rendering), но не ограничивается этим случаем использования. Scope содержит независимую копию всех юнитов (включая связи между ними) и основные методы для доступа к ним.

Scope можно создать с помощью fork.

Императивные вызовы эффектов с использованием scope

При выполнении императивных вызовов эффектов внутри обработчиков эффектов это поддерживается, но не внутри функций watch. Для обработчиков эффектов, которые вызывают другие эффекты, убедитесь, что вы вызываете только эффекты, а не обычные асинхронные функции. Кроме того, вызовы эффектов должны быть ожидаемыми:

✅ Правильное использование эффекта без вложенных эффектов:

const delayFx = createEffect(async () => {
  await new Promise((resolve) => setTimeout(resolve, 80));
});

✅ Правильное использование эффекта с вложенными эффектами:

const authUserFx = createEffect();
const sendMessageFx = createEffect();

const sendWithAuthFx = createEffect(async () => {
  await authUserFx();
  await delayFx();
  await sendMessageFx();
});

❌ Неправильное использование эффекта с вложенными эффектами:

const sendWithAuthFx = createEffect(async () => {
  await authUserFx();

  // Неправильно! Это должно быть обернуто в эффект.
  await new Promise((resolve) => setTimeout(resolve, 80));

  // Контекст здесь теряется.
  await sendMessageFx();
});

Для сценариев, когда эффект может вызывать другой эффект или выполнять асинхронные вычисления, но не то и другое одновременно, рассмотрите использование метода attach для более лаконичных императивных вызовов.

Потеря scope

Чем грозит вызов эффектов после асинхронных функций? Состояние, в которое попадает приложение после подобного вызова называется “потеря скоупа”, это означает, что после завершения вызова обычной асинхронной функции, все последующие действия попадут в глобальный режим (это то, что работает при прямом вызове $store.getState()), то есть все обновления данных не попадут в scope в котором велась работа, и как следствие, клиенту отправится неконсистентное состояние

Императивные вызовы эффектов в этом плане безопасны, потому что effector запоминает scope в котором начинался императивный вызов эффекта и при завершении вызова восстанавливает его обратно, что позволяет сделать ещё один вызов подряд

Можно вызывать методы Promise.all([fx1(), fx2()]) и прочие из стандартного api javascript, потому что в этих случаях вызовы эффектов по прежнему происходят синхронно и скоуп безопасно сохраняется

Все правила рассказанные для эффектов так же относятся и к императивным вызовам эвентов

Как обойти это ограничение?. Есть ситуации, когда вызова вне scope избежать нельзя, типичные примеры это setInterval и history.listen. Для того, чтобы безопасно передать в эти функции эффект (или эвент) можно воспользоваться методом scopeBind, он создаёт функцию, привязанную к скоупу в котором метод был вызван, позволяя безопасно вызывать её в дальнейшем

const sendWithAuthFx = createEffect(async () => {
  // Теперь эту функцию можно безопасно вызывать
  // без соблюдения правил потери скоупа
  const sendMessage = scopeBind(sendMessageFx);

  await authUserFx();

  // Контекста внутри setInterval нет, но наша функция привязана
  return setInterval(sendMessage, 500);
});
Важно помнить

Не забывайте очищать setInterval после завершения работы со скоупом во избежания утечек памяти. Очищать setInterval можно отдельным эффектом, предварительно вернув из первого эффекта его id и сохранив в отдельный стор

Можно ли как-то обойти потерю скоупа? Это проблема именно эффектора?. Это общий принцип работы с асинхронностью в JavaScript, все технологии, которые сталкиваются с необходимостью сохранения контекста в котором происходят вызовы так или иначе обходят это затруднение. Самый характерный пример это zone.js, который для сохранения контекста оборачивает все асинхронные глобальные функции вроде setTimeout или Promise.resolve. Также способами решения этой проблемы бывает использование генераторов или ctx.schedule(() => asyncCall()).

Будет ли общее для всех решение проблемы потери контекста? Да. Новый proposal в язык под названием async context призван решить эту проблему один раз и для всех, он позволит запустив асинхронную логику один раз, получать данные из контекста во всех связанных с ней вызовах, как бы они не происходили. Как только предложение войдёт в язык и получит широкую поддержку, effector обязательно переключится на это решение и правила вызовов эффектов уйдут в прошлое

Методы

.getState($store)

Возвращает значение хранилища в данном Scope.

Формулы

const scope: Scope;
const $value: Store<T> | StoreWritable<T>;

const value: T = scope.getState($value);

Возвращает

T значение хранилища

Примеры

Создайте два экземпляра приложения, вызовите события в них и проверьте значение хранилища $counter в обоих экземплярах:

import { createStore, createEvent, fork, allSettled } from "effector";

const inc = createEvent();
const dec = createEvent();
const $counter = createStore(0);

$counter.on(inc, (value) => value + 1);
$counter.on(dec, (value) => value - 1);

const scopeA = fork();
const scopeB = fork();

await allSettled(inc, { scope: scopeA });
await allSettled(dec, { scope: scopeB });

console.log($counter.getState()); // => 0
console.log(scopeA.getState($counter)); // => 1
console.log(scopeB.getState($counter)); // => -1

Попробовать

Перевод поддерживается сообществом

Документация на английском языке - самая актуальная, поскольку её пишет и обновляет команда effector. Перевод документации на другие языки осуществляется сообществом по мере наличия сил и желания.

Помните, что переведенные статьи могут быть неактуальными, поэтому для получения наиболее точной и актуальной информации рекомендуем использовать оригинальную англоязычную версию документации.

Соавторы