From Redux to MobX

Matti Bar-Zeev - Sep 13 '21 - - Dev Community

Step 1: A simple state

Introduction

MobX is a state management library and quite a popular one.
In this post I will attempt to convert a single simple state of my Word Search React Game from Redux to MobX while having both Redux and MobX co-exist on the same application. I hope you will find the following useful when you’re about to do the same.

Background

The word-search game is state driven, which means that everything in that game is a direct result of a state snapshot - collecting worlds, answering, editing etc. It is currently all powered by Redux, which is a great state management solution, but has its own caveats, like the boilerplate code one must need to introduce to the application.
I’m going to jump into this by converting the basics of our state - the game score. Whenever a word is found a point gets added to the score and when we reset the game, the score gets reset as well.

Go

In the current Redux implementation the score reducer has 2 actions it listens for:

  • RESET_GAME_ACTION - when we reset the score back to zero
  • ADD_POINT_ACTION - adding a point to the total score

I “detach” the Redux score reducer from the application, so that no score will be updated or reset. I do that by removing the score reducer from the combined reducers in the main app file.
No updates now. Nice.

We open up the MobX docs and see how we’re getting started with it...

So as I guessed we are first installing MobX and Mobx-React with npm install mobx mobx-react.
Nice thing about MobX is that its state is an object, which I find more intuitive than some abstract “virtual” state object that the reducers build implicitly.
I will create my application state, which is called “WordSearchGameState”. In this state I add the score member, the addPoint and reset action methods. It looks like this:

import {makeObservable, observable, action} from 'mobx';

const INITIAL_SCORE = 0;

export default class WordSearchGameState {
   score = INITIAL_SCORE;

   constructor() {
       makeObservable(this, {
           score: observable,
           addPoint: action,
           reset: action,
       });
   }

   addPoint() {
       this.score++;
   }

   reset() {
       this.score = INITIAL_SCORE;
   }
}
Enter fullscreen mode Exit fullscreen mode

Now I need to instantiate this state in the main application file:

...

const wordSearchGameState = new WordSearchGameState();
Enter fullscreen mode Exit fullscreen mode

There are a few ways to hand the state to nested components in react, and I’d like to go with the context approach. Besides the fact that the Mobx team recommends it, it appears to be the most elegant solution to do so IMO.
I create a context and wrap my App component with it, so now it is wrapped both by the Redux store context and with the Mobx state context -

...

export const StateContext = createContext();

const render = () => {
   ReactDOM.render(
       <Provider store={gameStore}>
           <StateContext.Provider value={wordSearchGameState}>
               <App />
           </StateContext.Provider>
       </Provider>,
       rootElement
   );
};
Enter fullscreen mode Exit fullscreen mode

I’m exporting the StateContext so that I can import it from whichever module that needs it and use it with useContext hook (see further below for more details).

The Masthead component is where the score is displayed, so let’s modify that one and add the means to obtain the score state from Mobx -
I first wrap the Masthead component with the observer HoC from mobx-react to allow it to listen for changes in MobX state. Now I bring the Mobx state context by using the useContext hook with the previously made context

const Masthead = observer(() => {
   const stateContext = useContext(StateContext);

Now Im replacing the previous score which came from Redux store with the new Mobx one:

// const score = useSelector((state) => state.score);
   const score = stateContext.score;
Enter fullscreen mode Exit fullscreen mode

Noice! We now have the score displayed on the game’s Masthead, but alas when we find a new word, it does not update with an additional point. I’m on it -

The component which is incharge of updating the score is WordsPanel. This is the panel where all the available words sit, ready to be found (in theory, the check should not be there but let’s work with what we’ve got at the moment).
Upon a correct find, the component dispatches a Redux event to add a point to the score, but we would like to change it to the MobX way, which means, call the addPoint action method on the game state.
To do that I import the game state context to the component and call this method when needed. Pretty straight forward, I’d say.
Here how it looks:

const WordsPanel = () => {
    const stateContext = useContext(StateContext);
    ...
if (found) {
    // dispatch(addPoint());
        stateContext.addPoint();
Enter fullscreen mode Exit fullscreen mode

And there we have it - score updated.

Now we need to address the issue of resetting the score.
I’m looking for the action which resets the score, and it is the RESET_GAME_ACTION. It is a generic action which some reducers listen to, one of them being the score reducer.
Adding to that is the fact that the reset action is an action which is pending on the user’s confirmation.
The confirmation mechanism I’ve built (you can read more about it here) supports only a single pending action, nothing more, and this means that we cannot inject any other operation to it.
This challenge would not exist if I had converted the entire application to work with MobX, but I think that's a good obstacle to tackle to get a good sense of what it means working in such a hybrid-state management mode.
Let’s continue...

To summarize what the confirmation action does, it sets a message to be displayed and then a pending action to be dispatched if the user confirms.
It seems like the way to go here is to add a pendingConfirmationCallback property to this Redux action. This way I will be able to add an arbitrary callback to any confirmation without jeopardizing the existing functionality. I feel that the need for a callback, regardless of the pending action, is something that can boost the flexibility of this confirmation mechanism with a little code addition. Kind of an enhancement I’m glad to do anyhow. I know it is not totally related to what we discuss here, but still.

So my onRefreshGame handler which gets invoked when the user clicks the “refresh” button, currently looks like this - I still have the Redux action dispatched once the user confirms, but I also invoke a callback function, which is my MobX reset() action, to reset the score.

function onRefreshGame() {
       const pendingConfirmationAction = resetGame();
       const pendingConfirmationCallback = stateContext.reset.bind(stateContext);
       const confirmResetGameAction = createConfirmAction({
           pendingConfirmationAction,
           msg: 'All progress will reset. Are you sure you wanna refresh the game?',
           pendingConfirmationCallback,
       });
       dispatch(confirmResetGameAction);
   }
Enter fullscreen mode Exit fullscreen mode

If I was to use Mobx solely, then I’d only need to call the reset action method and let it do all that is required. Notice that I’m binding the Mobx action to the Mobx state object to avoid scope errors.

And that’s it. When I refresh the game the score gets reset and everything works as it used to, only now the score state is being handled by MobX.

Epilogue

In this post we went over migrating a simple application state from Redux to Mobx, while having Redux still alive. My take from this process is that it is pretty easy to introduce MobX to an already state managed application, and nothing prevents it from co-existing with Redux, at least in this naive use-case brought here.

Cheers

Hey! If you liked what you've just read be sure to also visit me on twitter :) Follow @mattibarzeev 🍻

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .