How I migrated from Redux and Saga to MobX and became a happier person
Tags: mobx, redux, saga, react
Published: 2020-07-16 19:26:50

---
title: How I migrated from Redux and Saga to MobX and became a happier person
published: true
description: For quite a while I was using Redux together with Redux Saga and was satisfied. Well, sort of, because I was constantly asking myself "Should the state management always be that complex?" Redux was a great library for it's time, when the concept of a centralized state was not well-coined yet. Nevertheless, one day came, and I have decided it was time to move.
tags: mobx, redux, saga, react
cover_image: https://gannochenko.dev/static/229a96448f0e66938b19c9036192150e/77467/cover.jpg
canonical_url: https://gannochenko.dev/blog/how-i-migrated-from-redux-and-saga-to-mobx-and-became-a-happier-person
---

For quite a while I was using `Redux` together with `Redux Saga` and was satisfied. Well, sort of, because I was constantly asking myself "Should the state management always be that complex?"
`Redux` was a great library for it's time, when the concept of a centralized state was not well-coined yet. Nevertheless, one day came, and I have decided it was time to move.

## Requirements

In my applications I had:

* one special reducer that keeps data of the currently authenticated user, feature flags and some other different stuff,
* one reducer per page, combined all together with `combineReducers`,
* one saga per page,
* a way to observe and affect the state outside of the rendering context,
* a way to keep routing history and state in sync.

So will see how I'll tackle all of this with `MobX`. But first, a short intro to `MobX` is required.

### The State

According to `MobX`, a state is the heart of an application.

As a substitution for reducers I have decided to make several sub-states. So, there is a "root" state called `State`:

```ts
import { observable, computed, configure } from 'mobx';
import { Nullable } from '../../type';

// pre-configure MobX to have better experience
// see https://mobx.js.org/refguide/api.html
configure({ enforceActions: 'observed', computedRequiresReaction: true });

export class State {
    @observable public loading = false;
    @observable public error: Nullable<Error> = null;

    @computed get ready(): boolean {
        return this.loading || !this.error;
    }
}
```

The `MobX` state is not just a plain object. The state object has a prototype, which automatically enables the ultimate fun with all that OOP stuff: getters / setters, methods, incapsulation and so on. I am not a huge fan of OOP though, but occasionally it may be helpful.

You may have already noticed a couple of decorators back there.

* [@observable](https://mobx.js.org/refguide/observable-decorator.html) decorator tells `MobX` to observe this particular property for changes,
* [@computed](https://mobx.js.org/refguide/computed-decorator.html) decorator tells that there is a getter returning a value that is dependent on some observed properties and does not have side effects. This enables memoization.   

### Consuming the state

First thing first, I need to inject my state into components. React context and hooks are to my rescue here!

```ts
import React, { useContext } from 'react';

import { State } from './state';
import { Nullable } from '../../type';

type NullableState = Nullable<State>;

export type StatePropsType = {
    state: State;
};

export const StateContext = React.createContext<NullableState>(null);

export const StateProvider = StateContext.Provider;

export const useGlobalState = () => useContext<NullableState>(StateContext);
```

Then I modify my `Providers` component like this:

```ts
import React, { FunctionComponent } from 'react';

import { State } from '../../state/state';
import { StateProvider } from '../../state/context';

const state = new State();

export const Providers: FunctionComponent = ({ children }) => (
    <StateProvider value={state}>
        {children}
    </StateProvider>
);
```

And finally, everywhere I want this state consumed I just use `useGlobalState` hook:

```ts
import React, { useEffect, FunctionComponent } from 'react';

import { ApplicationProps } from './type';
import { useGlobalState } from '../state/context';

export const Application: FunctionComponent<ApplicationProps> = () => {
    const state = useGlobalState()!;

    return (
        <div>
            MobX is cool!
            <Loader state={state} />
        </div>
    );
};
```

### Reacting to state changes

Having the state consumed does not automatically mean the component will react on it's changes. However, just like Redux has a connector module `react-redux`, there is a module called `mobx-react`.

```ts
import { observer } from 'mobx-react';

const Loader = observer(({ state }: StatePropsType) => <span>{state.ready ? '' : 'Loading...'}</span>);
```

A few important things here:

* [observer](https://mobx.js.org/refguide/observer-component.html) is a wrapper function that makes our component "sensitive" to state changes. This is an analog of `connect()` from 'react-redux'.
* There is also an [@observer](https://mobx.js.org/refguide/observer-component.html) decorator, but it works only with `class components` (which I use less and less frequently as time goes by).
* I shall keep my `observers` as tiny as possible, to avoid re-rendering of different neighboring chunks of UI that do not care about the state at all.

### Changing the state

There are different ways to get the state changed. Personally, I prefer doing it Redux-way: via actions. But actions in `MobX` is nothing like in `Redux`: I don't need to dispatch anything anywhere hopefully :) An action here is just a method of the `State` class.

Let's modify the `State` class by adding an action or two!

```ts
import { observable, computed, configure, action } from 'mobx';
import { Nullable } from '../../type';

// pre-configure MobX to have better experience
// see https://mobx.js.org/refguide/api.html
configure({ enforceActions: 'observed', computedRequiresReaction: true });

export class State {
    @observable public loading = false;
    @observable public error: Nullable<Error> = null;

    @computed get ready(): boolean {
        return this.loading || !this.error;
    }

    @action.bound
    public start() {
        this.error = null;
        this.loading = true;
    }

    @action.bound
    public stop(error?: Error) {
        if (error) {
            this.error = Error;
        }
        this.loading = false;
    }
}
```

Here we have an [@action.bound](https://mobx.js.org/refguide/action.html) decorator (*bound* means that I can safely use `this` inside).

Then nothing stops me from calling these two beautiful actions to change the state:

```ts
import React, { useEffect, FunctionComponent } from 'react';

import { ApplicationProps } from './type';
import { useGlobalState } from '../state/context';

export const Application: FunctionComponent<ApplicationProps> = () => {
    const state = useGlobalState()!;

    return (
        <div>
            MobX is cool!<br />
            <button onClick={() => state.start()}>Start</button>{'  '}<button onClick={() => state.stop()}>Start</button><br />
            <Loader state={state} />
        </div>
    );
};
```

### Asynchronous actions

While `@action.bound` works nicely for sync actions, for the async ones things turn a bit more complicated.

It might seem so at the beginning, that I just need to add `async / await` to an action, and that will be enough. Well, yeh, technically it solves the case, but what if I need for something more reliable?
The problem with `async / await` is that I don't really have any control over the flow, and if an action takes longer than anticipated initially to execute, I might get into race condition troubles there.

So consider the following situation: 

1. A user opens a page. 
2. The data starts to load, the user is looking at the loading indicator meanwhile.
3. The user leaves the page.
4. It takes some time to load the data due to poor network connection.
5. The data arrives, the state gets updated even if the displayed page is not the same the data was obtained for.
6. ???

Is there a way to tell our async action, that we are leaving the page and no longer interested in the data?

Yes.

This is where `generators` come out. The coolest thing about a generator yielding promises is that it forms an async process which can be interrupted in the middle, right between two `yield` operators. From the developers perspective, a generator looks like just a normal function.

So this is how I have implemented this.

### Sub-states

I have decided to make a sub-state for each page of my application, just like I used to have separate reducers in `Redux` before. That is my home page for instance:

```ts
import React, { FunctionComponent, useEffect } from 'react';

import { Layout } from '../../components';
import { useGlobalState } from '../../state/context';

export const HomePage: FunctionComponent = () => {
    const state = useGlobalState()!;
    const { homePage } = state;

    useEffect(() => {
        homePage.onLoad();

        return () => homePage.onUnload();
    }, [homePage]);

    return (
        <Layout>
            Honey, I am home!
        </Layout>
    );
};
```

When I enter or leave the page, the `onLoad()` or `onUnload()` is called respectively.

And now the sub-state itself:

```ts
import { action, observable, flow, isFlowCancellationError } from 'mobx';
import { CancellablePromise } from 'mobx/lib/api/flow';
import { Nullable, ObjectLiteral } from '../../../type';

export class HomePageState {
    @observable ready = false;
    @observable loading = false;
    @observable error: Nullable<Error> = null;

    private queryLoad: Nullable<CancellablePromise<unknown>> = null;

    onLoad() {
        if (this.queryLoad) {
            // something is already being loaded for this page. Non-relevant, abort
            this.queryLoad.cancel();
        }

        // start new query
        this.queryLoad = this.startLoading();
        this.queryLoad.catch((error) => {
            if (!isFlowCancellationError(error)) {
                // the query was not cancelled, some other error popped up
                console.error(error);
            }
        });
    }

    onUnload() {
        if (this.queryLoad) {
            // we are leaving, stop whatever you are doing now
            this.queryLoad.cancel();
            this.queryLoad = null;
        }
        this.reset();
    }

    @action.bound
    reset(): void {
        this.ready = false;
        this.loading = false;
        this.error = null;
    }

    // that is our async process
    startLoading = flow(function* startLoading() {
        // @ts-ignore who cares?:)
        const self = this as HomePageState;

        self.loading = true;
        self.error = null;
        self.ready = false;

        // load something really heavy
        yield new Promise((resolve) => {
            setTimeout(resolve, 5000);
        });

        yield self.finishLoading();
    });

    @action.bound
    finishLoading(error?: Error): void {
        this.loading = false;

        if (error) {
            this.error = error;
        }

        this.ready = true;
    }
}
```

Okay, so [flow](https://mobx.js.org/best/actions.html) is the cake here. Since it is a generator, it can be cancelled between yields. So cool.

Flow here acts as a direct analog of `Redux Saga`.

I inject a sub-state into the main `State` like this:

```ts
import { observable, computed, configure, action } from 'mobx';
import { Nullable } from '../../type';
import { HomePageState } from '../../pages/HomePage';

// pre-configure MobX to have better experience
// see https://mobx.js.org/refguide/api.html
configure({ enforceActions: 'observed', computedRequiresReaction: true });

export class State {
    @observable public loading = false;
    @observable public error: Nullable<Error> = null;

    public homePage = new HomePageState();

    @computed get ready(): boolean {
        return this.loading || !this.error;
    }

    @action.bound
    public start() {
        this.error = null;
        this.loading = true;
    }

    @action.bound
    public stop(error?: Error) {
        if (error) {
            this.error = Error;
        }
        this.loading = false;
    }
}
```

## Conclusion

That is basically it! For me the codebase shrank significantly. All the cryptic reducers and the "plain object" state concept is now gone. This all brings me to the following outcome:

### Pros of MobX

* `MobX` is (almost) completely decoupled from the rendering cycle. For me it is a plus, since personally I don't like the do-everything-in-the-component approach. IMO, React should only know how to render the UI, and everything else should be delegated to other parties. Unfortunately, this can also manifest itself as a drawback (see below).
* `State`, `Reducer` and `Action` entities are now united into one single entity called `State`. It just feels more natural. I can also treat the entire thing as a blackbox when needed.
* All the complexity introduced by `Redux` is no longer on the picture (Phewww...)

### Cons of MobX

* Decorators work only with class components. If you need to turn a function component into an observer, you need to use alternative `observer()` syntax, which does not please the eye at all.
* `MobX` is (almost) completely decoupled from `React`. It means that one day `MobX` may become ideologically incompatible with potentially upcoming `React` features.
* The state changes are not discrete, and this fact makes the system more difficult to debug.
* Therefore, No time travel debugging and undo ability

I have prepared a [proof of concept repo](https://github.com/gannochenko/poc_mobx) to show the thing in action :)

## Alternatives

If `MobX` does not suit you well, there are many-many alternatives out there. Just check them out.

* [Apollo link state](https://www.apollographql.com/docs/link/links/state/)
* [Overmind](https://overmindjs.org/)
* [Recoil](https://recoiljs.org/) by Facebook
* [Context API](https://reactjs.org/docs/context.html)
* [Unstated-next](https://github.com/jamiebuilds/unstated-next)
* ... and many others :)

----

I hope it was helpful. So, what do you think of MobX? [Text me a message to discuss!](https://www.linkedin.com/in/gannochenko/)

* Header photo by Kin Li on Unsplash