Skip to content

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.

  1. 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.

ts
import { createEvent, fork, allSettled } from "effector";

const appStarted = createEvent();

const scope = fork();

await allSettled(appStarted, { scope });
  1. 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.

ts
import { createEvent, fork, allSettled } from "effector";

const appStarted = createEvent();

const scope = fork();

await allSettled(appStarted, { scope });
  1. Trigger start Event on the patricular Scope

allSettled function allows you to start an Event on particular Scope and wait until all computations will be finished.

ts
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:

ts
// 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.

ts
// 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.

ts
// 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.

ts
// 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

Released under the MIT License.