The best part of Effector
Tags: effector, javascript, frontend, statemanagement
Published: 2021-10-04 03:59:09

---
title: The best part of Effector
published: true
description: Effector is a lightweight and performant state manager. It is UI frameworks agnostic, predictable and easy to use. Almost half a year ago, Aviasales team has started migration from RxJS to Effector. While I was investigating this library, I found a completely unexpected feature. It changed my mind about Effector.
tags: effector, javascript, frontend, statemanagement
---

Effector is a lightweight and performant state manager. It is UI frameworks agnostic, predictable and easy to use. Almost half a year ago, Aviasales team has started migration from RxJS to Effector. While I was investigating this library, I found a completely unexpected feature. It changed my mind about Effector.

## Disclaimer

In my opinion, in modern applications, we have to write domain-specific logic **with** reactive primitives. Such primitives do not exist in JavaScript, so we must use a library for it. I prefer to use a state manager as a dependency zero and bound it with core application logic.

## The Problem

Creating complex business scenarios frequently includes waiting for all computations to be completed. Moreover, if an application is built over event-oriented architecture, it will be quite difficult to define the end of events processing. In the common case, we need this opportunity in two situations. The first one is widely used, any good application requires it. The second one is more specific, but it is pretty important too.

### Tests

In my everyday work, I write two types of tests: unit tests and domain-logic tests. Unit tests do not relate to state managers and the business logic of the application. However, the domain logic tests can lead to a problem.

In domain-specific scenarios we commonly use the same pattern — emit some event, wait for application reactions and relay on result state, e.g., 👇

```tsx
describe('User flow', () => {
  test('should set default currency after login', async () => {
    emitEvent('Login', { login, password })

    // ... wait

    expect(userSettings.currency).toBe('THB')
  }) 
})
```

If the whole authorization flow is synchronous, we do not have any problems — scenarios will end right after the first event emitting. Nonetheless, in real applications, almost all scenarios are completely asynchronous.

Furthermore, even in simple cases we can find a situation where simple waiting for some promise is impossible, e.g., 👇

```tsx
async function fetchCurrency({ token }) {
  const { currency } = await request('/settings', { token })

  return currency ?? 'THB'
}

async function login({ login, password }) {
  const token = await request('/login', { login, password })
  
  // can't wait for resolve
  // it's a domain-specific logic 🤷‍♂️
  fetchCurrency({ token })
    .then(currency => setLocalCurrency(currency))

  return token
}
```

Many business scenarios contain asynchronous operations, and some of these operations can cause other asynchronous operations, etc.

![Image 24.09.2021, 10-30.01f66b7057964c02ae08b25d2f2342e1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/38jsd1mo5wwkg7kd4jul.png)

The only solution for this case to wait for the end of scenario is to emit a special event. Thereafter, we can rewrite our tests this way 👇

```tsx
describe('User flow', () => {
  test('should set default currency after login', async () => {
    emitEvent('Login', { login, password })

    await waitForEvent('Login/Finished')

    expect(userSettings.currency).toBe('THB')
  }) 
})
```

But this solution prevents us from writing complex and well-designed scenarios. We will not be able to inverse dependencies and remove knowledge of the outer world from the scenario. It leads to gigantic scenarios, and engineers cannot keep it in their heads.

In an ideal case, I would not like to edit the source code of my scenario to satisfy tests.

```tsx
describe('User flow', () => {
  test('should set default currency after login', async () => {
    emitEvent('Login', { login, password })

    await waitForAllComputationsFinished()

    expect(userSettings.currency).toBe('THB')
  }) 
})
```

👆 this test case knows nothing about the internal structure of the login process. When you read it, you immediately understand what is happening — the user starts the login flow, and after the whole all process currency is changed to Thai bahts.

### SSR

> SSR (Server-side rendering) is a process of creating an HTML string based on current state of the application. The HTML is sent to the browser and the user can start using the site before JS loads. It is a good way to improve user experience and SEO.

Let us dive into the rendering process. Right after the user has opened a page, the application should request data, wait for the end of computation and pass the whole state to a UI-framework to create an HTML-string. This process looks very similar to the test of domain-specific scenarios (here we recognize “the user has opened a page” as a scenario).

```tsx
async function renderAppOnServer(route) {
  const store = createStore()

  emitEvent('Route/changed', { route })

  // ... wait

  return renderAppToString(store)
}
```

*All modern UI frameworks have an opportunity to create an interface as a function of the state. In this article, I will not dive deep into the details of a particular UI framework, let us use the magical generic function `renderAppToString`. It accepts the application state and returns an HTML-string. Implementation of this function is trivial for any popular UI framework.*

So, let us imagine the ideal world again! In the ideal world, the application ought to wait till the computations are completed on state and render a string 👇

```tsx
async function renderAppOnServer(route) {
  const store = createStore()

  emitEvent('Route/changed', { route })

  await waitForAllComputationsFinished()

  return renderAppToString(store)
}
```

## Classic Solutions

We have identified a problem of waiting for computations to be completed, so let us see how classic state managers solve it. I have an experience with `redux` and `MobX`, so I will talk about it. If your favourite state manager can handle this case simpler, please tell me about it on [Twitter](https://twitter.com/kamyshev_dev?s=21).

### Redux

First, redux itself does not have any mechanism to handle asynchronous actions and side effects. A common application uses something like `redux-saga` or `redux-thunk` (now it is a part of `@reduxjs/toolkit`) for it.

The simplest way to detect the end of computations is to add the new action “computations is ended”. It is a simple and working solution, but it has a fatal problem — you (as an engineer) should think about “end-of-computations” actions in any scenario, you should adopt a domain-specific logic to it. Moreover, if the scenario becomes more complex, this approach can destroy its readability.

Another option is to put the whole scenario logic to a single entity (`thunk`, `saga`, whatever). In this case, we can just wait for the end of the entity. E.g., thunk returns a simple promise from a `dispatch`-call, so we can wait for resolve. Sagas are based on generators and for handling this case they have a special library — [redux-saga-test-plan](https://github.com/jfairbank/redux-saga-test-plan).

In conclusion, redux-world has a solution. However, this solution is not simple and enjoyable 🤷‍♂️ also, it does not work in complex cases.

### MobX

Actually, MobX uses the same techniques as Redux for the solution of our problem. E.g., we can just add a boolean property to the store and wait for its changes 👇

```tsx
describe('User flow', () => {
  test('should set default currency after login', async () => {
    userStore.login({ login, password })

    await when(() => userStore.done)

    expect(userStore.currency).toBe('THB')
  }) 
})
```

So, it is working, except for one thing. We cannot use this solution for a complex scenario, if it works  with many stores.

Moreover, we can put the whole scenario in single asynchronous function, it will simplify the tests 👇

```tsx
describe('User flow', () => {
  test('should set default currency after login', async () => {
    await userStore.login({ login, password })

    expect(userStore.currency).toBe('THB')
  }) 
})
```

But it cannot cover complex scenarios.
In conclusion, MobX-world has a solution. However, this solution is not simple and enjoyable 🤷‍♂️ also, it does not work in complex cases.

## Effector-world

> To understand this part of the article, it is better to read [Effector’s documentation](https://effector.dev/) first.
Effector has a game-changer feature — **Fork API**. To understand it, we should talk about one important concept — `scope`.

The scope is an independent copy of the whole application. You can run any logic over a specific scope, and it will not affect any other scope. Let us read some code 👇

```tsx
const loginFx = createEffect(/* e.g., request to backend */)

// Event of currency change
const changeCurrency = settings.createEvent()

// Currency store
const $currency = createStore()
  // just save the payload of event to a store
  .on(changeCurrency, (_, newCurrency) => newCurrency)

sample({
  // After login request successfully ends
  source: loginFx.doneData,
  // get a currency from a result
  fn: ({ settings }) => settings.currency ?? 'thb',
  // and can event changeCurrency with it
  target: changeCurrency,
})
```

Now, we can *fork* this application and get an independent copy of the application — *scope*.

### Tests

Let us write a test for this scenario — after a user has logged in without specified currency, they should get Thai bahts as a currency.

```tsx
describe('User flow', () => {
  test('should set default currency after login', () => {
    loginFx({ login, password })

    expect($currency.getState()).toBe('THB')
  }) 
})
```

👆this test will fail, of course. It does not include waiting of the computation end.

In Effector-world, we can fix it with a special function `allSettled`. It starts a unit (event or effect) and waits for end of computations on the specified scope. To get a state of store in particular scope, we can use `scope.getState` method.

```tsx
describe('User flow', () => {
  test('should set default currency after login', async () => {
    // Fork application and create an isolated scope
    const scope = fork()

    // Start logixFx on the scope
    // and wait for computations env
    await allSettled(loginFx, {
      params: { login, password },
      scope,
    })

    // Check a store state on the scope
    expect(scope.getState($currency)).toBe('THB')
  }) 
})
```

So, we have written a test for domain-specific scenarios, and we did not edit the scenario for it. In my opinion, *it is the most important feature of Effector*.

#### One more thing

Yeah, you can notice that this test case executes a real effect-handler. It is a good note, we should mock the handler of `loginFx` and return some test data.

We can do this with test-runner mechanisms, e.g., replace imports or monkey-patch internal state of `loginFx`. I dislike these ways. It is too fragile because tests get a knowledge of internal structure of scenario.

**Fork API** helps us in this case, too. It has a built-in mechanism to replace any effect-handler in a specific scope. Let us improve our test-case 👇

```tsx
describe('User flow', () => {
  test('should set default currency after login', async () => {
    const scope = fork({
      handlers: new Map([
        // Replace original handler in this scope
        [loginFx, jest.fn(() => ({ settings: null }))]
      ])
    })

    await allSettled(loginFx, {
      params: { login, password },
      scope,
    })

    expect(scope.getState($currency)).toBe('THB')
  }) 
})
```

This feature helps us to replace any handlers without runtime modifications in specific test-case.

*Be careful! To use this feature, you should set up [official babel-plugin](https://effector.dev/docs/api/effector/babel-plugin/).*

### SSR

The second use-case of **Fork API** is an SSR. There are two reasons for it.

First, for SSR, the application executes in the Node.js environment. This environment can handle a huge amount of parallel requests. It means that we should isolate different instances of application between requests. If Effector-world, we should just fork application for any requests. So, each of the requests has a personal scope.

The second reason is `allSettled`. After requests, the application should start data-fetching and after the finish, it ought to render an interface based on specific scope.

E.g., this small application must load the counter from the internet and show it on the page:

```tsx
const routeChanged = createEvent()

const fetchUsersFx = createEffect(/* some request */)

const $userCount = stats.createStore()
  .on(fetchUsersFx.doneData, (_, newCount) => newCount)

guard({
  // When route changed
  clock: routeChanged,
  // if the new route is main page
  filter: (route) => route === 'main',
  // load users data
  target: fetchUsersFx,
})
```

👆 this scenario knows nothing about context. An application does not care whether it is executed in the user’s browser or in the Node.js environment on the server.

In this case, we can easily add SSR to the application. We have to fork the application on every request and execute computations using the isolated scope 👇

```tsx
async function renderAppOnServer(route) {
  // Create a scope for a specific request
  const scope = fork()

  // Emit an event about route changes
  // and wait for all computations
  await allSettled(routeChanged, {
    params: route,
    scope,
  })
  
  // All UI-framework's job
  return renderAppToString(scope)
}
```

*Effector has adapters for UI-frameworks to simplify scope-based rendering. Read the documentation for details.*

So, we have added an SSR to the application without changing the code. And, in my opinion, *it’s the second great feature of Effector*.

## So?

So, we decided to use Effector because it is based on multi-stores. It helps to create more solid applications and develop them in large teams. I thought, that it was a nice and interesting tool, but I was not a zealot of Effector.

However, after a month of development I had found Fork API, and it changed everything. Now I am confident that it is the best way to write applications without much effort.

---- 

Waiting for your comments and feedback 🤗