A comparison of state management in React with Mobx vs State lifting
Tags: mobx, react, tutorial
Published: 2020-12-02 13:52:43

This post is an attempt to compare state management between Mobx and the standard state lifting method on a tiny example. It exposes a starter's viewpoint on how both methods can be used and focuses on not using classes  since most if not all examples of code use classes. As we conclusion, we observe the rendering profiling which in this example is in favor of Mobx.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ntvue1eqdpfxgeps90z7.png)


> Cost of Mobx
The import cost is 45 kB.

Then it may be interesting to quote Mobx: "using observables inside React components adds value as soon as they are either 1) deep, 2) have computed values or 3) are shared with other observer components". 

Let's work on the tiny example proposed by  [Mobx's todo list](https://mobx.js.org/getting-started). This example handles a list of items where:
- every item can be checked or not,
- checking will change the style of the item (additional feature)
- we can add a new item,
- and we count the number of unchecked items.

We define 5 components: App, TodoListView, TodoView, NewTodo and the TodosCount. We also use the tiny library [clsx](https://yarnpkg.com/package/clsx) for conditional class rendering.
```
<App/>
  |- <TodosCount />
  |- <NewTodo />
  |- <TodoListView />
           |- <TodoView />
```

{% codesandbox mobx-mobxctc-proplift-87iyv%}


# Mobx version 

### Debugging tool

We may want to enable the very useful debugging config to track errors (unneeded decorators, unwatched actions...) by using:
```js
import { configure } from "mobx";
configure({
  enforceActions: "always",
  computedRequiresReaction: true,
  reactionRequiresObservable: true,
  observableRequiresReaction: true,
  disableErrorBoundaries: true
});
```
For example, if you get a message like "Derivation observer is created/updated without reading any observable value", this means that some component is wrongly decorated with  `observer`. 
My strategy was basically to wrap every component with `observer` and then remove the warnings by putting `action` on events.

### Domain store

We define our [domain store](https://mobx.js.org/defining-data-stores.html#domain-stores), the list of todos. It is an object that will be proxied by the `observable` method. Here we remove the logic from the components and move it into the store. 

This "store" contains:
-  the attribute `todos=[]`, an array of objects in the form:

  `{ id: Math.random(), title: "first", finished: false }`
- three functions:  a `getter` named "unfinished" that returns just a value, and two `action` "addTodo" and "toggle" that modifies the store. 

The functions that mutate directly the state within the "store" are wrapped by an `action`. This is the big difference with pur React: we don't have to write pur functions where we use copies of the state to manipulate it.

Wrapping it with `observable` defines what Mobx should monitor.

```js
# mobx-store.js
import { observable, action } from "mobx";

const store = observable({
  todos: [],
  get unfinished() {
    return this.todos.filter((todo) => todo.finished === false).length;
  },
  addTodo: action((todo) => store.todos.push(todo)),
  toggle: action((todoid) => {
    const id = store.todos.findIndex((todo) => todo.id === todoid);
    return (store.todos[id].finished = !store.todos[id].finished);
  }),
});
```
We instantiate the store by using the 'addTodo' method we created:
```js
store.addTodo({ id: Math.random(), title: "first", finished: false });
...
```
The imports:
```js
import { observer } from "mobx-react-lite";
import store from './mobx-store';
import clsx from "clsx";
import "./index.css";
```

### Decorate Components with `observer`

The arrow functions components will be proxied (or not) with the `observer` decorator to create a reactive context. The rule is: Mobx should only read observable within an observer component. The debug config helps to define which component should be proxied with observer.

With the closure/import, the store is available within each component. However, Mobx recommends to [pass object references around as long as possible](https://mobx.js.org/react-integration.html#tip-grab-values-from-objects-as-late-as-possible).

Then not everything should be handled by the store. Mobx encourages to use local state with `React.useState` whenever [local state is needed](https://mobx.js.org/react-integration.html#you-might-not-need-locally-observable-state).

This proxying makes the code cleaner compared to the state lifting technique where we have to explicitly pass down the references to the methods. It is also shorter compared to the `useContext` hook. We just use the methods defined in the "store" where needed.  
This component uses observable values to render so we wrap it with `observer`. Also Mobx asks to [wrap events with action](https://mobx.js.org/actions.html#wrapping-functions-using-action) as used here.
```js
const TodoView = observer(({ todoList,todo }) => {
  const mystyle = clsx({ ischecked: todo.finished,
    notchecked: !todo.finished,
  });
  return (
    <>
      <li>
        <label htmlFor={todo.title} className={mystyle}>
          <input
            type="checkbox"
            id={todo.title}
            defaultChecked={todo.finished}
            onChange={action(() => todoList.toggle(todo.id))}
          />
          {todo.title}
        </label>
      </li>
    </>
  );
});
```
The todo creation uses the method `addTodo` defined in the store. Note that the debug config asks to remove the  decorator because the component doesn't use any value from the "store" to render. However, we modify the "store" on the submit event so again we have to use an `action` to wrap the event.

```js
const NewTodo = ({todoList}) => {
  const [newTitle, setNewTitle] = React.useState("");
  return (
    <form
      onSubmit={action((e) => {
        e.preventDefault();
        todoList.addTodo({ title: newTitle, id: Math.random(), finished: false });
        setNewTitle("");
      })}
    >
      <input
        type="text"
        value={newTitle}
        onChange={(e) => setNewTitle(e.target.value)}
      />
      <input type="submit" value="New item" />
    </form>
  );
};
```
The component that renders the list of todos is a classic map. This component obviously uses the values to render so needs to be wrapped:
```js
const TodoListView = observer(({ todoList }) => {
  return (
    <ul>
      {todoList.todos && todoList.todos.map((todo) => <TodoView todo={todo} key={todo.id} todoList={todoList}/>)}
    </ul>
  );
});
```
This component renders the count of unchecked todos and calls the  'unfinished' method defined in the store. Again the component uses a value to render so we wrap it.

```js
const TodosCount = observer(({todoList) => {
  return <h3>Mobx: UnFinished todos count: {todoList.unfinished}</h3>;
});
```

Mobx recommends to '[grab values from objects as late as possible](We pass the whole todoList object as a prop since )': instead of passing `store.todos`, we pass the "store" object to the higher component "App" and cascade down. Since this component doesn't use the "store" values to render, there is no need to wrap with `observer`.
```js
export default function AppMobx () {
  return(
   <>
    <TodosCount todoList={store}/> 
    <NewTodo todoList={store} />
    <TodoListView todoList={store} />
   </>
  )
};
```
In conclusion, I just followed the debug config to eventually remove the warnings and this works.


# State lifting method
With the method, the higher component App will handle state, namely the 'todos': it is an array of objects in the form `{id:number, title:string, finished: boolean}`. While with Mobx the actions are in the store and we just call them where needed, here the actions that modify state are defined in the higher component and we pass references along to the children.

For the component that renders each todo, we just use the reference to the function 'toggle':
```js
const TodoView = ({ todo, onToggle }) => {
  const mystyle = clsx({
    ischecked: todo.finished,
    not checked: !todo.finished,
  });
  return (
    <li>
      <label htmlFor={todo.title} className={mystyle}>
        <input
          type="checkbox"
          id={todo.title}
          defaultChecked={todo.finished}
          onChange={() => onToggle(todo.id)}
        />
        {todo.title}
      </label>
    </li>
  );
};
```
This todo creation is very similar:
```js
function NewTodo({ onhandleAddTodo }) {
  const [newTitle, setNewTitle] = React.useState("");
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        onhandleAddTodo({ title: newTitle, id: Math.random(), finished: false });
        setNewTitle("");
      }}
    >
      <input
        type="text"
        value={newTitle}
        onChange={(e) => setNewTitle(e.target.value)}
      />
      <input type="submit" value="Submit" />
    </form>
  );
}

```
and the todolist rendering identical:
```js
const TodoListView = ({ todoList, onhandleToggle }) => {
  return (
      <ul>
        {todoList &&
          todoList.map((todo) => (
            <TodoView todo={todo} key={todo.id} onToggle={onhandleToggle} />
          ))}
      </ul>
  );
};
```

```js
function TodosCount({ count }) {
  return <h3>State lift: UnFinished todos count: {count}</h3>;
}
```

The higher component holds state, the todos. All the actions that modify state are defined here.
```js
const AppStateLift = React.memo(() => {
  const [todos, setTodos] = React.useState(initList);
  const [count, setCount] = React.useState(0);

  React.useEffect(() => {
    setCount(todos.filter((todo) => todo.finished === false).length);
  }, [todos]);

  function hanleToggle(id) {
    setTodos((previous) => {
      const foundId = previous.findIndex((todo) => todo.id === id);
      const todoAtFoundId = previous[foundId];
      const newTodos = [...previous];
      newTodos[foundId] = {
        ...todoAtFoundId,
        finished: !todoAtFoundId.finished,
      };
      return newTodos;
    });
  }

  function handleAddTodo(todo) {
    setTodos((previous) => [...previous, todo]);
  }

  return (
    <div>
      <TodosCount/>
      <NewTodo onhandleAddTodo={handleAddTodo} />
      <TodoListView
        todoList={todos}
        onhandleToggle={handleToggle}
      />
    </div>
  );
});
```
and finally:
```js
ReactDOM.render(
  <div>
    <AppMobx />
    <AppStateLift />
  </div>,
  document.getElementById("app")
);
```
# Conclusion
Now that we have the code, we can run the same sequence on each version and observe the profiling in the dev tools. I clicked successively on all checkboxes and entered a new todo. You may find a big difference: Mobx seems to memoize and render the minimum, compared to the React rendering. Here is the result.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ntvue1eqdpfxgeps90z7.png)


Thanks for reading!