Потеря скоупа

Работа юнитов в effector всегда происходит внутри скоупа — глобального или изолированного, созданного через fork(). В глобальном случае контекст потерять невозможно, так как он используется по умолчанию. С изолированным скоупом всё сложнее: при потере скоупа операции начнут выполняться в глобальном режиме, а все обновления данных не попадут в скоуп в котором велась работа, и как следствие, клиенту отправится неконсистентное состояние.

Типичные места, где это проявляется:

Пример проблемы

Мы создадим простой таймер на 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>
);
}

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

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

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

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

Соавторы