Изолированный контекст

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

import { fork, allSettled } from "effector";
// Создаем новый скоуп
const scope = fork();
const $counter = scope.createStore(0);
const increment = scope.createEvent();
$counter.on(increment, (state) => state + 1);
// Запускаем событие и дожидаемся всей цепочки выполнения
await allSettled(increment, { scope });
console.log(scope.getState($counter)); // 1
console.log($counter.getState()); // 0 - оригинальный стор остается без изменений

С помощью fork мы создаем новый скоуп, а с помощью allSettled — запускаем цепочку событий внутри указанного скоупа и дожидаемся ее завершения.

Независимость скоупов

Не существует механизма для обмена данными между скоупами; каждый экземпляр полностью изолирован и работает самостоятельно.

Зачем нужен cкоуп?

В effector все состояние хранится глобально. В клиентском приложении (SPA) это не проблема: каждый пользователь получает собственный экземпляр кода и работает со своим состоянием. Но при серверном рендеринге (SSR) или параллельном тестировании глобальное состояние становится проблемой: данные одного запроса или теста могут “протечь” в другой. Поэтому нам необходим скоуп.

  • SSR — сервер работает как единый процесс и обслуживает запросы множества пользователей. Для каждого запроса можно создать скоуп, который изолирует данные от глобального контекста Effector и предотвращает утечку состояния одного пользователя в запрос другого.
  • Тестирование — при параллельном запуске тестов возможны гонки данных и коллизии состояний. Скоуп позволяет каждому тесту выполняться со своим собственным изолированным состоянием.

У нас есть подробные гайды по работе с серверным рендерингом (SSR) и тестировании, а здесь мы сосредоточимся на основных принципах работы со скоупом, его правилах и способах избежать распространенных ошибок.

Правила работы со скоупом

Для корректной работы со скоупом имеется ряд правил, чтобы избежать потери скоупа:

Вызов эффектов и промисов

Для обработчиков эффектов, которые вызывают другие эффекты, убедитесь, что вы вызываете только эффекты, а не обычные асинхронные функции. Кроме того, вызовы эффектов должны быть ожидаемыми (awaited).

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

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

// ✅ правильное использование эффекта без вложенных эффектов
const delayFx = createEffect(async () => {
await new Promise((resolve) => setTimeout(resolve, 80));
});
// ✅ правильное использование эффекта с вложенными эффектами
const authFx = createEffect(async () => {
await loginFx();
await Promise.all([loadProfileFx(), loadSettingsFx()]);
});
// ❌ неправильное использование эффекта с вложенными эффектами
const sendWithAuthFx = createEffect(async () => {
await authUserFx();
//неправильно! Это должно быть обернуто в эффект.
await new Promise((resolve) => setTimeout(resolve, 80));
// здесь скоуп теряется.
await sendMessageFx();
});
attach в деле

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

Использование юнитов с фреймворками

Всегда используйте хук useUnit в связке с фреймворками, чтобы effector сам вызвал юнит в нужном ему скоупе:

import { useUnit } from "effector-react";
import { $counter, increased, sendToServerFx } from "./model";
const Component = () => {
const [counter, increase, sendToServer] = useUnit([$counter, increased, sendToServerFx]);
return (
<div>
<button onClick={increase}>{counter}</button>
<button onClick={sendToServer}>send data to server</button>
</div>
);
};

Ну все, хватит слов, давайте посмотри на то как это работает.

Использование в SSR

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

  • Пользователь А делает запрос → на сервере в $notifications загружаются его уведомления.
  • Почти одновременно Пользователь B делает запрос → стор перезаписывается его данными.
  • В результате оба получат список уведомлений Пользователя B.

Получилось явно не то, что мы хотели, да ? Это и есть состояние гонки, ведущее к утечке приватных данных. В этой ситуации скоуп обеспечит нам изолированный контекст, который будет работать только для текущего пользователя: Пользователь сделал запрос -> создался скоуп и теперь мы меняем состояние только в нашем скоупе, так будет работать для каждого запроса.

server.tsx
import { renderToString } from "react-dom/server";
import { fork, serialize, allSettled } from "effector";
import { Provider } from "effector-react";
import { fetchNotificationsFx } from "./model";
async function serverRender() {
const scope = fork();
// Загружаем данные на сервере
await allSettled(fetchNotificationsFx, { scope });
// Рендерим приложение
const html = renderToString(
<Provider value={scope}>
<App />
</Provider>,
);
// Сериализуем состояние для передачи на клиент
const data = serialize(scope);
return `
<html>
<body>
<div id="root">${html}</div>
<script>window.INITIAL_DATA = ${data}</script>
</body>
</html>
`;
}

Что стоит отметить в этом примере:

  1. Мы сериализовали данные с помощью метода serialize, чтобы корректно передать их на клиент.
  2. На клиенте мы гидрировали сторы с помощью аргумента конфигурации values у fork.
Перевод поддерживается сообществом

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

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

Соавторы