Explicit start of the app
In Effector Events can not be triggered implicitly. It gives you more control over the app's lifecycle and helps to avoid unexpected behavior.
The code
In the simplest case, you can just create something like appStarted
Event and trigger it right after the app initialization. Let us pass through the code line by line and explain what's going on here.
- Create start Event
This Event will be used to trigger the start of the app. For example, you can attach some global listeners after this it.
import { createEvent, fork, allSettled } from "effector";
const appStarted = createEvent();
const scope = fork();
await allSettled(appStarted, { scope });
- Create isolated Scope
Fork API allows you to create isolated Scope which will be used across the app. It helps you to prevent using global state and avoid unexpected behavior.
import { createEvent, fork, allSettled } from "effector";
const appStarted = createEvent();
const scope = fork();
await allSettled(appStarted, { scope });
allSettled
function allows you to start an Event on particular Scope and wait until all computations will be finished.
import { createEvent, fork, allSettled } from "effector";
const appStarted = createEvent();
const scope = fork();
await allSettled(appStarted, { scope });
The reasons
The main reason for this approach is it allows you to control the app's lifecycle. It helps you to avoid unexpected behavior and make your app more predictable in some cases. Let us say we have a module with the following code:
// app.ts
import { createStore, createEvent, sample, scopeBind } from 'effector';
const $counter = createStore(0);
const increment = createEvent();
const startIncrementationIntervalFx = createEffect(() => {
const boundIncrement = scopeBind(increment, { safe: true });
setInterval(() => {
boundIncrement();
}, 1000);
});
sample({
clock: increment,
source: $counter,
fn: (counter) => counter + 1,
target: $counter,
});
startIncrementationIntervalFx();
Tests
We believe that any serious application has to be testable, so we have to isolate application lifecycle inside particular test-case. In case of implicit start (start of model logic by module execution), it will be impossible to test the app's behavior in different states.
TIP
scopeBind
function allows you to bind an Event to particular Scope, more details you can find in the article about Fork API rules.
Now, to test the app's behavior, we have to mock setInterval
function and check that $counter
value is correct after particular time.
// app.test.ts
import { $counter } from './app';
test('$counter should be 5 after 5 seconds', async () => {
// ... test
});
test('$counter should be 10 after 10 seconds', async () => {
// ... test
});
But, counter will be started immediately after the module execution, and we will not be able to test the app's behavior in different states.
SSR
In case of SSR, we have to start all application's logic on every user's request, and it will be impossible to do with implicit start.
// server.ts
import * as app from './app';
function handleRequest(req, res) {
// ...
}
But, counter will be started immediately after the module execution (aka application initialization), and we will not be able to start the app's logic on every user's request.
Add explicit start
Let us rewrite the code and add explicit start of the app.
// app.ts
import { createStore, createEvent, sample, scopeBind } from 'effector';
const $counter = createStore(0);
const increment = createEvent();
const startIncrementationIntervalFx = createEffect(() => {
const boundIncrement = scopeBind(increment, { safe: true });
setInterval(() => {
boundIncrement();
}, 1000);
});
sample({
clock: increment,
source: $counter,
fn: (counter) => counter + 1,
target: $counter,
});
startIncrementationIntervalFx();
const appStarted = createEvent();
sample({ clock: appStarted, target: startIncrementationIntervalFx });
That is it! Now we can test the app's behavior in different states and start the app's logic on every user's request.
TIP
In real-world applications, it is better to add not only explicit start of the app, but also explicit stop of the app. It will help you to avoid memory leaks and unexpected behavior.
One more thing
In this recipe, we used application-wide appStarted
Event to trigger the start of the app. However, in real-world applications, it is better to use more granular Events to trigger the start of the particular part of the app.
Recap
- Do not execute any logic just on module execution
- Use explicit start Event of the application