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.
The basic flow in MobX is:
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.
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.
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.
This is actually the easiest part.
import { GameService } from 'app/services/game.service';
@Component({
...
})
export class ControlsComponent {
constructor(private game:GameService) { }
}
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.
OnPush
change detection strategy, we can gain extremely high performance.autorun
function.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.
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!