DEV Community

Sirwan Afifi
Sirwan Afifi

Posted on

MobX with React and TypeScript

MobX is one of the popular state management libraries. One of the great things about MobX is that we can store state in a simple data structure and allow the library to take care of keeping everything up to date. The MobX API is pretty simple; in fact, it has these four simple building blocks at its core:

  • Observable
  • Actions
  • Computed
  • Reactions

Observable

The idea is that when the data changes, the observable object notifies the observers. To define a property as observable, all we need to do is to use @observable decorator:



class TodoStore {
  @observable todos: Todo[]
}


Enter fullscreen mode Exit fullscreen mode

Now When a new value is assigned to todos array, the notifications will fire, and all the associated observers will be notified.

Actions

Action is a way to change an observable (update the state). To define an action, we decorate methods inside the store with @action:



@action toggleTodo = (id: string) => {
    this.todos = this.todos.map(todo => {
      if (todo.id === id) {
        return {
          ...todo,
          completed: !todo.completed
        };
      }
      return todo;
    });
};


Enter fullscreen mode Exit fullscreen mode

Computed

Computed can be used to derive values from the existing state or other computed values:



@computed get info() {
    return {
      total: this.todos.length,
      completed: this.todos.filter(todo => todo.completed).length,
      notCompleted: this.todos.filter(todo => !todo.completed).length
    };
}


Enter fullscreen mode Exit fullscreen mode

Reactions

Reactions track observables from inside the store itself. In the example below if the action to set todos is called, then it runs the second argument.



class TodoStore {
  constructor() {
    reaction(
      () => this.todos,
      _ => console.log(this.todos.length)
    );
  }


Enter fullscreen mode Exit fullscreen mode

Creating a Simple Todo App with MobX and React

Now that we have talked about the main concepts, let's create a simple todo app using React, MobX and TypeScript:

Alt Text

So go to the terminal, create a directory then CD into it then type in this command to scaffold a new React project:



npx create-react-app . --typescript


Enter fullscreen mode Exit fullscreen mode

For this project, I am using Bootstrap so let's add it as a dependency to the newly created project:



yarn add bootstrap --save


Enter fullscreen mode Exit fullscreen mode

Now go to index.tsx and import bootstrap.css:



import "bootstrap/dist/css/bootstrap.css"


Enter fullscreen mode Exit fullscreen mode

Now we'll install the needed dependencies:



yarn add mobx mobx-react-lite uuid @types/uuid --save


Enter fullscreen mode Exit fullscreen mode

The next thing we have to do is to set experimentalDecorators flag to true in tsconfig.json in order for the MobX decorators to compile properly:



{
  "compilerOptions": {
    // other stuff...

    "experimentalDecorators": true
  }
}


Enter fullscreen mode Exit fullscreen mode

After all the above stuff is done, we have MobX ready to go. Now let's create a store inside the project src/stores/TodoStore.ts. Add the following code to it:



import { observable, action, computed, reaction } from "mobx"
import { createContext } from "react"
import uuidv4 from "uuid/v4"

export interface Todo {
  id?: string;
  title: string;
  completed: boolean;
}

class TodoStore {
  constructor() {
    reaction(() => this.todos, _ => console.log(this.todos.length))
  }

  @observable todos: Todo[] = [
    { id: uuidv4(), title: "Item #1", completed: false },
    { id: uuidv4(), title: "Item #2", completed: false },
    { id: uuidv4(), title: "Item #3", completed: false },
    { id: uuidv4(), title: "Item #4", completed: false },
    { id: uuidv4(), title: "Item #5", completed: true },
    { id: uuidv4(), title: "Item #6", completed: false },
  ]

  @action addTodo = (todo: Todo) => {
    this.todos.push({ ...todo, id: uuidv4() })
  }

  @action toggleTodo = (id: string) => {
    this.todos = this.todos.map(todo => {
      if (todo.id === id) {
        return {
          ...todo,
          completed: !todo.completed,
        }
      }
      return todo
    })
  }

  @action removeTodo = (id: string) => {
    this.todos = this.todos.filter(todo => todo.id !== id)
  }

  @computed get info() {
    return {
      total: this.todos.length,
      completed: this.todos.filter(todo => todo.completed).length,
      notCompleted: this.todos.filter(todo => !todo.completed).length,
    }
  }
}

export default createContext(new TodoStore())


Enter fullscreen mode Exit fullscreen mode

Now create a new folder called components in the src directory and add TodoAdd.tsx and TodoList.tsx.

TodoAdd



import React, { useContext, useState } from "react"
import TodoStore from "../stores/TodoStore"
import { observer } from "mobx-react-lite"

const AddTodo = () => {
  const [title, setTitle] = useState("")
  const todoStore = useContext(TodoStore)
  const { addTodo, info } = todoStore

  return (
    <>
      <div className="alert alert-primary">
        <div className="d-inline col-4">
          Total items: &nbsp;
          <span className="badge badge-info">{info.total}</span>
        </div>
        <div className="d-inline col-4">
          Finished items: &nbsp;
          <span className="badge badge-info">{info.completed}</span>
        </div>
        <div className="d-inline col-4">
          Unfinished items: &nbsp;
          <span className="badge badge-info">{info.notCompleted}</span>
        </div>
      </div>
      <div className="form-group">
        <input
          className="form-control"
          type="text"
          value={title}
          placeholder="Todo title..."
          onChange={e => setTitle(e.target.value)}
        />
      </div>
      <div className="form-group">
        <button
          className="btn btn-primary"
          onClick={_ => {
            addTodo({
              title: title,
              completed: false,
            })
            setTitle("")
          }}
        >
          Add Todo
        </button>
      </div>
    </>
  )
}

export default observer(AddTodo)


Enter fullscreen mode Exit fullscreen mode

TodoList



import React, { useContext } from "react";
import TodoStore from "../stores/TodoStore";
import { observer } from "mobx-react-lite";

const TodoList = () => {
  const todoStore = useContext(TodoStore);
  const { todos, toggleTodo, removeTodo } = todoStore;
  return (
    <>
      <div className="row">
        <table className="table table-hover">
          <thead className="thead-light">
            <tr>
              <th>Title</th>
              <th>Completed?</th>
              <th>Actions</th>
            </tr>
          </thead>
          <tbody>
            {todos.map(todo => (
              <tr key={todo.id}>
                <td>{todo.title}</td>
                <td>{todo.completed ? "โœ…" : ""}</td>
                <td>
                  <button
                    className="btn btn-sm btn-info"
                    onClick={_ => toggleTodo(todo.id!)}
                  >
                    Toggle
                  </button>
                  <button
                    className="btn btn-sm btn-danger"
                    onClick={_ => removeTodo(todo.id!)}
                  >
                    Remove
                  </button>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </>
  );
};

export default observer(TodoList);



Enter fullscreen mode Exit fullscreen mode

Both components use observer which is an HOC to make the components observers of our store. So any changes to any of the observable will cause the React components to re-render.

Thatโ€™s it ๐Ÿš€ Youโ€™re now up and going with MobX in your React application.

Here's the source for the project.

Originally published at https://sirwan.info/blog

Top comments (2)

Collapse
 
razgvili profile image
RazGvili

Cool!
To run on local i needed to change the constructor a little.

I used makeAutoObservable instead of the @observable.

Collapse
 
aaronlam profile image
Aaronlam

Thanks your post, benefit a great deal.