Move from Mobx to Redux

Frank Ma
5 min readSep 7, 2017

--

Recently started a React project with Mobx, as I used to do Redux, I decided to go with Mobx this time just to try it out and see if it works better than Redux. After three months, we found that Mobx might not be good for big team as it is in lack of rules to follow. So we decided to move to Redux.

Our project is to help users fill in and file tax returns, the user interface looks like this.

The launch screen has client selection and the button to launch into return form page. The return form has three sub pages and a series of fields to be filled in.

Firstly we have to know what information we should put into the store. Here quote from the ‘Redux FAQ: Organizing State’ page, which also applies to Mobx.

Some common rules of thumb for determining what kind of data should be put into Redux:

Do other parts of the application care about this data?

Do you need to be able to create further derived data based on this original data?

Is the same data being used to drive multiple components?

Is there value to you in being able to restore this state to a given point in time (ie, time travel debugging)?

Do you want to cache the data (ie, use what’s in state if it’s already there instead of re-requesting it)?

So our application have the client store and the tax return store. Let’s have a look at our Mobx store.

class ClientStore {
@observable clients = [];
@observable currentIndex = null;
constructor() {
this.loadClients();
}
@computed get clientInfo() {
let clientInfo = this.clients[this.currentIndex] || {};
if (clientInfo.objInstID) {
localStorage.setItem('clientId', clientInfo.objInstID);
}
return clientInfo;
}
@action loadClients() {
let that = this;
request(url)
.then(action('loadClients-callback', function(result){
that.clients = result.data.data.reverse();
}));
}
@action setCurrentIndex(index) {
this.currentIndex = index;
}
}

Mobx store has decorators like ‘observable’, ‘computed’ and also ‘action’. ‘Observable’ is for attribute to be observed on. ‘Computed’ is for an attribute that is calculated based on a function, any variable referenced in the function is observed and will be updated on change. In my opinion, this is one of the shinning features of Mobx. ‘Action’ is for marking purpose, all the changes to store data should be done with functions marked as action.

After refactoring to Redux, clientStore looks like this:

import { loadIr9 } from './IR9Store';
const LOAD_CLIENTS = 'LOAD_CLIENTS';
const SET_ClIENT_ID = 'SET_ClIENT_ID';
// actions
export function loadClients() {
return dispatch => {
return request(url).then(result => {
dispatch({
type: LOAD_CLIENTS,
payload: result.data.data.reverse()
});
});
};
}
export function setClientId(clientId) {
return dispatch => {
localStorage.setItem('clientId', clientId);
dispatch(loadIr9(clientId));
dispatch({
type: SET_ClIENT_ID,
payload: clientId
});
};
}
// reducer
export default function(state = {
clients: [],
currentIndex: 0,
currentClientId: null,
currentClientInfo: {}
}, action) {
switch(action.type) {
case LOAD_CLIENTS:
return Object.assign({}, state, {clients: action.payload});
case SET_ClIENT_ID:
let newIndex = state.currentIndex;
let newClientId = state.currentClientId;
state.clients.map(function(client, index) {
if (client.objInstID == action.payload) {
newIndex = index;
newClientId = client.objInstID;
}
});
return Object.assign({}, state, {
currentIndex: newIndex,
currentClientId: newClientId,
currentClientInfo: state.clients[newIndex]
});
default:
return state;
}
}

We can see that we have more code now, but the logic looks clearer and we have better control over the logic. Reducers are only about data and data change, actions handle other logics such as request and local storage, actions talk to reducer with dispatch.

Now we have the stores. We should have a look at the integration process of the stores into the application.

Here is what we did with Mobx in main.js.

import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'mobx-react';
import routes from './routes';
import MainStore from './stores/MainStore';
let mainStore = new MainStore();
window.__myapp_container = document.getElementById('appContainer')
ReactDOM.render(
<Provider clientStore={mainStore.clientStore} ir9Store={mainStore.ir9Store}>
{routes}
</Provider>,
window.__myapp_container
);

We import Provider from mobx-react and wrap routes with the provider which has store attibutes.

In MainStore.js we have:

import { autorun } from 'mobx';
import ClientStore from './ClientStore';
import IR9Store from './IR9Store';
class MainStore {
constructor() {
this.clientStore = new ClientStore();
this.ir9Store = new IR9Store();
this.refreshIr9 = this.refreshIr9.bind(this);
this.disposeRefresh = autorun(this.refreshIr9);
}
refreshIr9() {
let that = this;
let client = that.clientStore.clientInfo;
if (client && client.objInstID) {
that.ir9Store.getIr9(client.objInstID);
}
}
}
export default MainStore;

After refactoring to Redux, our main.js like this.

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import configureStore from './configureStore';
import { loadClients } from './stores/ClientStore';
import routes from './routes';
const store = configureStore();
store.dispatch(loadClients());
window.__myapp_container = document.getElementById('appContainer');
ReactDOM.render(
<Provider store={store}> {routes} </Provider>,
window.__myapp_container
);

Similar to Mobx, we import Provider from react-redux and wrap routes with it.

In configureStore.js we have:

import { createStore, applyMiddleware } from 'redux';
import { createLogger } from 'redux-logger';
import thunkMiddleware from 'redux-thunk';
import rootReducer from './stores/MainStore';
const loggerMiddleware = createLogger();
export default function configureStore(preloadedState) {
return createStore(
rootReducer,
preloadedState,
applyMiddleware(
thunkMiddleware,
loggerMiddleware
)
);
};

And MainStore.js:

import { combineReducers } from 'redux';
import clientReducer from './ClientStore';
import ir9Reducer from './IR9Store';
export default combineReducers({
clientStore: clientReducer,
ir9: ir9Reducer
});

And now we arrive at the last bit, which is connect to the stores from component.

With Mobx, in the component we have:

import { inject, observer } from "mobx-react";export default @inject('clientStore', 'ir9Store') @observer class Home extends React.Component {

Then we can access the stores from the props simply like this this.props.clientStore and do actions like that.props.clientStore.setCurrentIndex(event.target.value).

After refactoring to Redux, we have components like this:

import { connect } from 'react-redux';
import { setClientId } from '../stores/ClientStore';
class Home extends React.Component {
onClientChange(event) {
let that = this;
that.props.dispatch(setClientId(event.target.value));
}
render() { }
}
function mapStateToProps(state) {
const { clientStore, ir9 } = state;
const { clients, currentClientId } = clientStore;
return { clients, currentClientId, ir9 };
}
export default connect(mapStateToProps)(Home);

We still access the store data from props but more flexible with configuration function ‘mapStatetoProps’. And we do actions by dispatching imported action from the stores. We can see onClientChange function that.props.dispatch(setClientId(event.target.value)) in above code.

Another difference I noticed is how to make changes to the store, as we know, Redux uses Immutable for store data, entire new state is returned every time on change. While with Mobx you can change store data any way you want, but if the store update is not dispatched as you expected, it might be hard for you to debug. And also if you want to detect store change in the component, with Redux you can just use componentWillReceiveProps, while with Mobx it might not work, in this case, you can try componentWillReact and autorun.

As a conclusion, we can see Mobx is more flexible and easy to use while Redux lets us follow more rules but gives us clearer logic and more control over the application.

--

--

Frank Ma
0 Followers

Web developer. Specialized in SPA and API.