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. Перевод документации на другие языки осуществляется сообществом по мере наличия сил и желания.
Помните, что переведенные статьи могут быть неактуальными, поэтому для получения наиболее точной и актуальной информации рекомендуем использовать оригинальную англоязычную версию документации.