Изолированный контекст
С помощью скоупов вы можете создать изолированный экземпляр всего приложения, который содержит независимую копию всех юнитов (включая их связи), а также базовые методы для работы с ними:
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)); // 1console.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
для более лаконичных императивных вызовов.
Использование юнитов с фреймворками
Всегда используйте хук 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.
Получилось явно не то, что мы хотели, да ? Это и есть состояние гонки, ведущее к утечке приватных данных. В этой ситуации скоуп обеспечит нам изолированный контекст, который будет работать только для текущего пользователя: Пользователь сделал запрос -> создался скоуп и теперь мы меняем состояние только в нашем скоупе, так будет работать для каждого запроса.
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>`;}
import { hydrateRoot } from "react-dom/client";import { fork } from "effector";
// гидрируем скоуп начальными значениямиconst scope = fork({ values: window.INITIAL_DATA,});
hydrateRoot( document.getElementById("root"), <Provider value={scope}> <App /> </Provider>,);
Что стоит отметить в этом примере:
- Мы сериализовали данные с помощью метода
serialize
, чтобы корректно передать их на клиент. - На клиенте мы гидрировали сторы с помощью аргумента конфигурации
values
уfork
.
Связанные API и статьи
- API
Scope
- Описание скоупа и его методовscopeBind
- Метод для привязки юнита к скоупуfork
- Оператор для создания скоупаallSettled
- Метод для вызова юнита в предоставленном скоупе и ожидания завершения всей цепочки эффектовserialize
- Метод для получения сериализованного значения сторовhydrate
- Метод для гидрации сериализованных данных
- Статьи
Документация на английском языке - самая актуальная, поскольку её пишет и обновляет команда effector. Перевод документации на другие языки осуществляется сообществом по мере наличия сил и желания.
Помните, что переведенные статьи могут быть неактуальными, поэтому для получения наиболее точной и актуальной информации рекомендуем использовать оригинальную англоязычную версию документации.