DEV Community

Cover image for Manage the state of your React app with MobX
Francisco Mendes
Francisco Mendes

Posted on

Manage the state of your React app with MobX

Overview

Thank god in the React universe we have a lot of solutions to manage the state of our applications. But many of these solutions bring concepts that we have to learn and to understand exactly what we are doing and why we are doing it.

And obviously we have similar solutions with the famous useState hook and the only difference between these solutions and the useState hook is that we can consume and interact with a global state.

But let's assume that we want a really simple solution, but one that is immensely flexible, with good documentation and performance. I would choose MobX without thinking twice.

With MobX I only need to worry about three things, define properties, define methods and consume data from my store in the components of my application. And implementing it is as simple as pure JavaScript.

Today's example

In today's example we're going to create a super simple application where we're going to add items to a list, we can remove items from that list, we can consume/read list data and we'll be able to know the number of elements present in that list.

Let's code

First let's install the following dependencies:

npm install mobx mobx-react
Enter fullscreen mode Exit fullscreen mode

Now let's create our store, there are several ways to implement it, I go for the simplest one, which is to work with classes.

// @src/store.js

class Store {

}

export default new Store();
Enter fullscreen mode Exit fullscreen mode

Our store will only contain one property that we'll name a list and it will be an empty array.

// @src/store.js

class Store {
  list = [];

}

export default new Store();
Enter fullscreen mode Exit fullscreen mode

Now let's create a method called addItem that will be responsible for inserting an element in our list. And this method will take a single argument which will be the text.

// @src/store.js

class Store {
  list = [];

  addItem = (text) => {
    this.list.push(text);
  };
}

export default new Store();
Enter fullscreen mode Exit fullscreen mode

Now let's create the method responsible for removing a specific item from our list according to the element's index.

// @src/store.js

class Store {
  list = [];

  addItem = (text) => {
    this.list.push(text);
  };

  removeItem = (index) => {
    this.list.splice(index, 1);
  };
}

export default new Store();
Enter fullscreen mode Exit fullscreen mode

Now we need to create a getter so we can get the value of our list property.

// @src/store.js

class Store {
  list = [];

  addItem = (text) => {
    this.list.push(text);
  };

  removeItem = (index) => {
    this.list.splice(index, 1);
  };

  get getListItems() {
    return this.list;
  }
}

export default new Store();
Enter fullscreen mode Exit fullscreen mode

Now we need to create another getter that will be responsible for returning the number of elements present in our list property.

// @src/store.js

class Store {
  list = [];

  addItem = (text) => {
    this.list.push(text);
  };

  removeItem = (index) => {
    this.list.splice(index, 1);
  };

  get getListItems() {
    return this.list;
  }

  get count() {
    return this.list.length;
  }
}

export default new Store();
Enter fullscreen mode Exit fullscreen mode

We basically already have our store done, however now we need to implement the MobX features to do all the magic for us and we just need to take these concepts into account:

  • observable - is a field that needs to be tracked;
  • action - is a function that mutates a field;
  • computed - it is a value that is dependent on the changes that are made in a field;

With this in mind we can import each of them into our store as well as makeObservable. Now let's use MobX to observe each of the changes/interactions made in our state. So our list property will be observable, while the addItem and removeItem methods will be actions. While the getter count will be computed.

// @src/store.js
import { action, computed, makeObservable, observable } from "mobx";

class Store {
  list = [];

  constructor() {
    makeObservable(this, {
      list: observable,
      addItem: action.bound,
      removeItem: action.bound,
      count: computed,
    });
  }

  addItem = (text) => {
    this.list.push(text);
  };

  removeItem = (index) => {
    this.list.splice(index, 1);
  };

  get getListItems() {
    return this.list;
  }

  get count() {
    return this.list.length;
  }
}

export default new Store();
Enter fullscreen mode Exit fullscreen mode

We can finally use our store on our components. Now let's pretend this is your component:

// @src/App.jsx
import React, { useState, useCallback } from "react";

const App = () => {
  const [text, setText] = useState("");

  const handleSubmit = useCallback(() => {
    if (text === "") return;
    setText("");
  }, [text]);

  const list = ["qwerty"];

  return (
    <div>
      <ul>
        {list.map((element, index) => (
          <li key={index} onClick={() => console.log(index)}>
            {element}
          </li>
        ))}
      </ul>
      <br />
      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder={list.length > 0 ? "Type something" : "Add the first item"}
      />
      <button onClick={handleSubmit}>Add Item</button>
      <br />
      <h3>Count: {list.length}</h3>
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

The first thing we're going to do is import the observer from mobx-react which is nothing less than a High Order Component (HOC) so we can use React together with Mobx and the way we're going to do it is simply wrap the component. In this case, just do it in the export.

// @src/App.jsx
import React, { useState, useCallback } from "react";
import { observer } from "mobx-react";

// ...

export default observer(App);
Enter fullscreen mode Exit fullscreen mode

Now we can import our store naturally and make some changes to our code. For example, in the handleSubmit function we can add the addItem method and pass as a single argument the value of the component's state, which is the text.

// @src/App.jsx
import React, { useState, useCallback } from "react";
import { observer } from "mobx-react";

import store from "./store";

const App = () => {
  const [text, setText] = useState("");

  const handleSubmit = useCallback(() => {
    if (text === "") return;
    store.addItem(text);
    setText("");
  }, [text]);

  const list = ["qwerty"];

  return (
    // ...
  );
};

export default observer(App);
Enter fullscreen mode Exit fullscreen mode

Now the list variable we have in our component has a hardcoded list and what we want is the data to come from our store.

// @src/App.jsx
import React, { useState, useCallback } from "react";
import { observer } from "mobx-react";

import store from "./store";

const App = () => {
  const [text, setText] = useState("");

  const handleSubmit = useCallback(() => {
    if (text === "") return;
    store.addItem(text);
    setText("");
  }, [text]);

  const list = store.getListItems;

  return (
    // ...
  );
};

export default observer(App);
Enter fullscreen mode Exit fullscreen mode

Another change we have to make is to replace the console log we have (which is invoked when we click on an item in the unordered list) with the removeItem method and let's pass the element's index as the only argument.

// @src/App.jsx
import React, { useState, useCallback } from "react";
import { observer } from "mobx-react";

import store from "./store";

const App = () => {
  // ...
  return (
    <div>
      <ul>
        {list.map((element, index) => (
          <li key={index} onClick={() => store.removeItem(index)}>
            {element}
          </li>
        ))}
      </ul>
      <br />
      // ...
    </div>
  );
};

export default observer(App);
Enter fullscreen mode Exit fullscreen mode

Now we have to make another two changes, one is to the placeholder and the other is to the heading. While we are directly getting the values of our list variable, we want to get the data coming from our getter count.

// @src/App.jsx
import React, { useState, useCallback } from "react";
import { observer } from "mobx-react";

import store from "./store";

const App = () => {
  // ...
  return (
    <div>
      // ...
      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder={store.count > 0 ? "Type something" : "Add the first item"}
      />
      <button onClick={handleSubmit}>Add Item</button>
      <br />
      <h3>Count: {store.count}</h3>
    </div>
  );
};

export default observer(App);
Enter fullscreen mode Exit fullscreen mode

Your component's final code should look like this:

// @src/App.jsx
import React, { useState, useCallback } from "react";
import { observer } from "mobx-react";

import store from "./store";

const App = () => {
  const [text, setText] = useState("");

  const handleSubmit = useCallback(() => {
    if (text === "") return;
    store.addItem(text);
    setText("");
  }, [text]);

  const list = store.getListItems;

  return (
    <div>
      <ul>
        {list.map((element, index) => (
          <li key={index} onClick={() => store.removeItem(index)}>
            {element}
          </li>
        ))}
      </ul>
      <br />
      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder={store.count > 0 ? "Type something" : "Add the first item"}
      />
      <button onClick={handleSubmit}>Add Item</button>
      <br />
      <h3>Count: {store.count}</h3>
    </div>
  );
};

export default observer(App);
Enter fullscreen mode Exit fullscreen mode

The final result of the application should look like this:

final app

Conclusion

As always, I hope you found it interesting. If you noticed any errors in this article, please mention them in the comments. 🧑🏻‍💻

Hope you have a great day! 🌴

Top comments (0)