Skip to content

Fork API rules ​

Fork API allows you to run multiple instances of the same application in the single process. It is useful for testing, SSR, and other cases. It is powerful mechanism, but it has some rules that you should follow to avoid unexpected behavior.

Prefer declarative code ​

All Effector's operators (like sample or combine) support Fork API out of the box, if you describe your application logic in a declarative way with Effector's operator, you do not have to do anything to make it work with Fork API.

Of course, in some cases, you have to use some logic without Effector's operators, in this case, you have to follow some rules.

Do not mix Effects and async functions ​

It is illegal to mix Effects and async functions inside Effect handler body. This code will lead to unexpected behavior:

ts
import { createEffect } from "effector";

async function regularAsyncFunction() {
  // do stuff
}

const asyncFunctionInFx = createEffect(async () => {
  // do other stuff
});

const doAllStuffFx = createEffect(async () => {
  await regularAsyncFunction(); // 🔴 regular async function
  await asyncFunctionInFx(); // 🔴 effect
});

Actually, it can be fixed in a simple way. Just wrap all async functions into Effects:

ts
import { createEffect } from "effector";

async function regularAsyncFunction() {
  // do stuff
}
const regularAsyncFunctionFx = createEffect(regularAsyncFunction);

const asyncFunctionInFx = createEffect(async () => {
  // do other stuff
});

const doAllStuffFx = createEffect(async () => {
  await regularAsyncFunctionFx(); // 🟢 effect
  await asyncFunctionInFx(); // 🟢 effect
});
One more thing

The last example is supported by Fork API, but there is a better way to do it. You can use sample operator to express the same logic:

ts
const doAllStuff = createEvent();

sample({ clock: doAllStuff, target: regularAsyncFunctionFx });
sample({ clock: regularAsyncFunctionFx.done, target: asyncFunctionInFx });

It is more declarative and expandable. For example, you can easily handle errors from this Effects independently:

ts
sample({ clock: regularAsyncFunctionFx.fail, target: logError });
sample({ clock: asyncFunctionInFx.fail, target: showErrorMessage });

Promise.all and Promise.race ​

Fork API supports Promise.all and Promise.race out of the box. You can use them in your code without any restrictions.

ts
const doAllStuffFx = createEffect(async () => {
  // 🟢 valid
  await Promise.all([regularAsyncFunctionFx(), asyncFunctionInFx()]);
});

const doRaceStuffFx = createEffect(async () => {
  // 🟢 valid
  await Promise.race([regularAsyncFunctionFx(), asyncFunctionInFx()]);
});

Bind Events to particular Scope ​

Another important rule is to bind Events to particular Scope if you call them from external sources outside the Effector. For example, if you pass them as a callback to some external library, or if you call them from the UI layer as an event handler.

useUnit ​

For UI-libraries (like SolidJS or React), Effector has a special hooks that help you to bind Events to the current Scope automatically:

ts
import { useUnit } from 'effector-solid';

const doStuff = createEvent();

function Component() {
  const handleClick = useUnit(doStuff);

  return <button onClick={handleClick}>Click me</button>;
}
ts
import { useUnit } from 'effector-react';

const doStuff = createEvent();

function Component() {
  const handleClick = useUnit(doStuff);

  return <button onClick={handleClick}>Click me</button>;
}

Also, you have to provide the current Scope to UI-library through the context. Read more about it in the official documentation.

scopeBind ​

However, sometimes you have to call Events from the external sources, for example, pass them as a callback to some external library or DOM APIs. In this case, you have to use scopeBind function:

ts
import { createEvent, createEffect, scopeBind, sample } from 'effector'

const windowGotFocus = createEvent();

const setupListenersFx = createEffect(async () => {
  const boundWindowGotFocus = scopeBind(windowGotFocus);
  addEventListener('focus', boundWindowGotFocus);
});

sample({ clock: appStarted, target: setupListenersFx });

TIP

In this example we have to scopeBind inside Effect because it contains current Scope. To call this Effect we use explicit application start Event.

Use explicit start of the application ​

The last rule is to use explicit start of the application. It is important because you have to provide the current Scope to the Effector itself. To fulfill this requirement, you can call start function with the current Scope through allSetteled method:

ts
import { allSettled } from 'effector';

await allSettled(appStarted, { scope });

Recap ​

  • One effect is one Effect, do not use asynchronous functions inside Effect body
  • Always use scopeBind for Events that are passed to external sources
  • Do not forget to use useUnit (or its analogs) for Events that are used in the UI layer
  • Do not execute any logic just on module execution, prefer explicit start of the application

Released under the MIT License.