1 post tagged

MobX

Tic-Tac-Toe Using Angular and MobX

Adam Klein

MobX solves the problem of state management in frontend apps, in a declarative, simple and performant way. It differs from other popular solutions by removing a lot of boilerplate, and allowing you to work with mutable data and OOP.

The best way to explain how it works is via a code example.
Our demo application is a tic-tac-toe game.

Full code and demo.

The basic flow in MobX is:

  • observables – define trackable attributes
  • computed – define derived state
  • actions – change the state
  • reactions – react to the change (update UI)

Plain objects

A store in MobX is just an object. It saves only the minimal state of our application. In our case, a 3X3 array representing the board.

class Game {  
  @observable board:string[][];
}

The @observbale decorator tells MobX it should track access to the board attribute. Note that this has nothing to do with rxjs’ observable, except for the name.

Computed values

Tell me your board, and I shall tell you who the current player is...

In our example, we will need a few other attributes of the game.
For example, who the current player is, who is the winner, how many moves left, etc. All of these values can be derived from the board. In MobX, this is called computed values:


  // Count the total number of cells that have a value
  @computed get moves():number {
    return this.board[0].filter(cell => cell).length +
      this.board[1].filter(cell => cell).length +
      this.board[2].filter(cell => cell).length;
  };

  // If 'moves' is even, then it's X turn, otherwise O
  @computed get currentPlayer():string {
    return this.moves % 2 ? 'X' : 'O';
  }

Computed values are just declarative functions that calculate derived properties. They are only recalculated if needed, and if they are being observed somewhere.

Actions

Changing the state in MobX is simple. It’s simply setting attributes on plain objects:

@action play(i,j) {
  this.board[i][j] = this.currentPlayer;
}

@action resetGame() {
  ...
}

The @action decorator tells MobX to run this function in a transaction. A transaction is a block a of code that recalculates computed values only when the block finishes, and not immediately when observables are set.

Connecting to Angular

This is actually the easiest part.

1. Inject the store:

import { GameService } from 'app/services/game.service';

@Component({
  ...
})
export class ControlsComponent {  
  constructor(private game:GameService) { }
}

2. Use the store inside the component as a regular object:

template: `
    <div *mobxAutorun>
      <h1 *ngIf="game.winner">{{ game.winner }} has won the game</h1>
      <button (click)="game.resetGame()">Reset Game</button>
    </div>
  `

The mobx-angular library offers a *mobxAutorun directive that automatically observes the values that we use inside our template.

The ControlsComponent uses the winner computed value, and invokes the resetGame action. Whenever a board cell is changed, MobX will recalculate the winner attribute, and update the component – telling it to re-render.

  • If we use OnPush change detection strategy, we can gain extremely high performance.
  • When the component is destroyed, the observer is automatically disposed.
  • Under the hood, this directive uses MobX autorun function.

How does this magic work?

MobX wraps each @observable value with custom getters and setters. When we access this.board[0][1] for example, MobX adds this observable to a dependency tree of the current computed value. Then, when we run something like: this.board[0][1] = ‘X’, MobX checks the dependency tree and recalculates all the relevant computed values. Then, it runs all the reactions that are dependent on all observables and computed values that changed.

Where do I start?

A good place to start is to read mobx and mobx-angular documentation.

Then, create a POC on your existing app. Choose a specific page of the app and move the state to a MobX store to see how easy and straightforward it is.

Good luck!

Angular   MobX