Scope loss
The execution of units in Effector always happens within a scope — either the global one or an isolated one created with fork()
. In the global case, the context cannot be lost, since it’s used by default. With an isolated scope, things are trickier: if the scope is lost, operations will start executing in the global mode, and all data updates will not enter the scope in which the work was conducted. As a result, an inconsistent state will be sent to the client.
Typical places where this happens:
setTimeout
/setInterval
addEventListener
WebSocket
- direct promise calls inside effects
- third-party libraries with async APIs or callbacks.
Example of the problem
We’ll create a simple timer in React, although the same behavior applies to any framework or runtime when scope loss occurs:
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 { 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> );}
Now let’s add an effect that calls tick
every second:
const startTimerFx = createEffect(() => { setInterval(() => { tick(); }, 1000);});
At first glance, the code looks fine, but if you start the timer, you’ll notice that the UI doesn’t update. This happens because timer changes occur in the global scope, while our app is running in the isolated scope we passed to <Provider>
. You can observe this in the console.
How to fix scope loss?
To fix scope loss, you need to use the scopeBind
function. This method returns a function bound to the scope in which it was called, and can later be safely executed:
const startTimerFx = createEffect(() => { const bindedTick = scopeBind(tick);
setInterval(() => { bindedTick(); }, 1000);});
Note that scopeBind
automatically works with the currently used scope. However, if needed, you can pass the desired scope explicitly as the second argument:
scopeBind(tick, { scope });
Don’t forget to clear setInterval
after finishing work to avoid memory leaks. You can handle this with a separate effect, return the interval ID from the first effect and store it in a dedicated store.
Why does scope loss happen?
Let’s illustrate how scope works in effector:
// our active scopelet scope;
function process() { try { scope = "effector"; asyncProcess(); } finally { scope = undefined; console.log("our scope is undefined now"); }}
async function asyncProcess() { console.log("we have scope", scope); // effector
await 1;
// here we already lost the context console.log("but here scope is gone", scope); // undefined}
process();
// Output:// we have scope effector// our scope is undefined now// but here scope is gone undefined
You might be wondering “Is this specifically an Effector problem?”, but this is a general principle of working with asynchronicity in JavaScript. All technologies that face the need to preserve the context in which calls occur somehow work around this difficulty. The most characteristic example is zone.js,
which wraps all asynchronous global functions like setTimeout
or Promise.resolve
to preserve context. Other solutions to this problem include using generators or ctx.schedule(() => asyncCall())
.
JavaScript is preparing a proposal Async Context, which aims to solve the context loss problem at the language level. This will allow:
Automatically preserving context through all asynchronous calls Eliminating the need for explicit use of scopeBind Getting more predictable behavior of asynchronous code
Once this proposal enters the language and receives wide support, Effector will be updated to use this native solution.
Related API and articles
- API
Effect
- Description of effects, their methods and propertiesScope
- Description of scopes and their methodsscopeBind
- Method for binding a unit to a scopefork
- Operator for creating a scopeallSettled
- Method for calling a unit in a given scope and awaiting the full effect chain
- Articles