DEV Community

Matti Bar-Zeev
Matti Bar-Zeev

Posted on

From Redux to MobX

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

Discussion (5)

Collapse
romeerez profile image
Roman Kushyn • Edited

Would be nice to mention that MobX doesn't force to use classes, and you can do all the same with functions.

Also to explain when to use context for a store and when it's not necessary: in next.js we need to instantiate store for each request, i.e for each user, individually, and without next.js, perhaps when using electron, we could use simple imports instead.

Also not quite clean what is the selling point of Mobx vs Redux, is it just boilerplate? Redux-toolkit decreases boilerplate amount a lot. Or maybe mobx indeed has a different philosophy, and, unlike redux, it's not forcing whole state of application to be global, so the store can be encapsulated withing a specific feature, and it's very easy to create many small stores

Collapse
mbarzeev profile image
Matti Bar-Zeev Author

Thanks for the reply.
Could you please elaborate or add a resource link to what you've mentioned about using functions instead of classes in MobX?
Also how do you see instantiating a store for each request, or using Electron affecting whether to use the context or not?
BTW, I am not attempting to "sell" MobX :) This is my way of getting to know it.
Appreciate your feedback.

Collapse
romeerez profile image
Roman Kushyn • Edited

Regarding functions approach, there is a useLocalObservable which is calling makeObservable under the hood. Example is here: mobx.js.org/react-integration.html...
(Click on "useLocalObservable hook" tab)
Another option is to use const store = useMemo(() => makeAutoObservable({ ...store definition }), [])
Because makeAuto is a bit different from make and is preferred in some cases.

So I create a file myFeature.store.ts and write a hook in there useMyFeatureStore where I call useLocalObservable, and then call this hook in component and put the store to context - when need a context. And when don't need it - just export const myStore = makeAutoObservable({ ...definition of store })

On one hand, well, maybe classes looks more accurate. But on the other hand, in React everything is usually written on functions and hooks and we get used to it.

Also how do you see instantiating a store for each request

When the store is instantiated in the component during rendering, we can be sure this store will be available only for current user of next.js. In Electron or when no next.js we can be sure there is only one user using the app, so it's usually fine to export const myStore = makeAutoObservable({ ...definition of store }) and to use the store by simply importing it from store file.

BTW, I am not attempting to "sell" MobX :)

I know, you are learning and sharing experience, and this is great. I just wish MobX to be more popular to not see Redux on every project.

Thread Thread
mbarzeev profile image
Matti Bar-Zeev Author

interesting... I still think that having the state in a separated class, which can be tested quite easily as well, is a better approach.
It's nice, though, that MobX gives the flexibility to split the application state according to business contexts, but I believe it is more applicable to large scale application.
Great input, I will keep on digging into this :)

Thread Thread
romeerez profile image
Roman Kushyn

It's nice, though, that MobX gives the flexibility to split the application state according to business contexts, but I believe it is more applicable to large scale application.

Well, let's imagine simplified dev.to: article with "like" button and comments. And two MobX stores are serving this: Comments.store.ts and ArticleButtons.store.ts. It's hard to name this site "large scale application", but still splitting business contexts makes it cleaner. So, in my opinion, the way you write react should not be different depending of the size of the app. Writing state of every aspect of the app to the single store is just the same as writing all JSX in the single App file.