Потеря скоупа
Работа юнитов в effector всегда происходит внутри скоупа — глобального или изолированного, созданного через fork(). В глобальном случае контекст потерять невозможно, так как он используется по умолчанию. С изолированным скоупом всё сложнее: при потере скоупа операции начнут выполняться в глобальном режиме, а все обновления данных не попадут в скоуп в котором велась работа, и как следствие, клиенту отправится неконсистентное состояние.
Типичные места, где это проявляется:
setTimeout/setIntervaladdEventListenerWebSocket- прямой вызов промисов в эффектах
- сторонние библиотеки с асинхронными API или колбэки.
Пример проблемы
Мы создадим простой таймер на React, хотя такая же модель поведения при потере скоупа будет соответствовать для любого фреймворка или среды:
import React from "react";import { createEvent, createStore, createEffect, scopeBind } from "effector";import { useUnit } from "effector-react";
const tick = createEvent();const $timer = createStore(0);
$timer.on(tick, (s) => s + 1);
export function Timer() { const [timer, startTimer] = useUnit([$timer, startTimerFx]);
return ( <div className="App"> <div>Timer:{timer} sec</div> <button onClick={startTimer}>Start timer</button> </div> );}import React from "react";import { Provider } from "effector-react";import { fork } from "effector";import { Timer } from "./timer";
export const scope = fork();
export default function App() { return ( <Provider value={scope}> <Timer /> </Provider> );}Теперь добавим эффект, который каждую секунду вызывает tick:
const startTimerFx = createEffect(() => { setInterval(() => { tick(); }, 1000);});Вот здесь можно потыкать пример.
На первый взгляд мы написали вполне рабочий код, но если запустить таймер, то вы заметите, что UI не обновляется. Это из-за того, что изменения таймера происходят в глобальном скоупе, а наше приложение работает в изолированном, который мы передали в <Provider>, вы можете это заметить по логам в консоли.
Как исправить потерю скоупа ?
Чтобы исправить исправить потерю скоупа нужно использовать функцию scopeBind. Этот метод возвращает функцию, привязанную к скоупу в котором метод был вызван, которую в последствии можно безопасно вызывать:
const startTimerFx = createEffect(() => { const bindedTick = scopeBind(tick);
setInterval(() => { bindedTick(); }, 1000);});Заметьте, что метод scopeBind сам умеет работать с текущим используемым скоупом. Однако, если вам нужно, то вы можете передать нужный скоуп в конфигурационный объект вторым аргументом.
scopeBind(tick, { scope });Не забывайте очищать setInterval после завершения работы во избежания утечек памяти. Очищать setInterval можно отдельным эффектом, предварительно вернув из первого эффекта его id и сохранив в отдельный стор.
Почему происходит потеря скоупа?
Давайте представим, то как работает скоуп в effector:
// наш активный скоупlet scope;
function process() { try { scope = "effector"; asyncProcess(); } finally { scope = undefined; console.log("наш скоуп undefined"); }}
async function asyncProcess() { console.log("у нас есть скоуп", scope); // effector
await 1;
// тут мы уже потеряли контекст console.log("а здесь скоупа уже нет ", scope); // undefined}
process();
// Вывод:// у нас есть скоуп effector// наш скоуп undefined// а здесь скоупа уже нет undefinedВозможно вас интересует вопрос “Это проблема именно эффектора?”, однако это общий принцип работы с асинхронностью в JavaScript, все технологии, которые сталкиваются с необходимостью сохранения контекста в котором происходят вызовы так или иначе обходят это затруднение. Самый характерный пример это zone.js,
который для сохранения контекста оборачивает все асинхронные глобальные функции вроде setTimeout или Promise.resolve. Также способами решения этой проблемы бывает использование генераторов или ctx.schedule(() => asyncCall()).
В JavaScript готовится proposal Async Context, который призван решить проблему потери контекста на уровне языка. Это позволит:
- Сохранять контекст автоматически через все асинхронные вызовы
- Избавиться от необходимости явного использования scopeBind
- Получить более предсказуемое поведение асинхронного кода
Как только это предложение войдет в язык и получит широкую поддержку, effector будет обновлен для использования этого нативного решения.
Связанные API и статьи
- API
Effect- Описание эффекта, его методов и свойствScope- Описание скоупа и его методовscopeBind- Метод для привязки юнита к скоупуfork- Оператор для создания скоупаallSettled- Метод для вызова юнита в предоставленном скоупе и ожидания завершения всей цепочки эффектов
- Статьи
Документация на английском языке - самая актуальная, поскольку её пишет и обновляет команда effector. Перевод документации на другие языки осуществляется сообществом по мере наличия сил и желания.
Помните, что переведенные статьи могут быть неактуальными, поэтому для получения наиболее точной и актуальной информации рекомендуем использовать оригинальную англоязычную версию документации.