MobX Strategies with React Hooks

Suraj KC
5 min readApr 19, 2020

--

React is a lightweight JS library for building rich and dynamic web applications. Instead of being opinionated on the application design and architecture, it leaves up to the developers to define structure that suit their needs. This flexibility comes with a cost of overwhelming choices of patterns, libraries and solutions. State management is a crucial part of any JS application. Selecting a state management solution can be confusing and difficult typically in react as opposed to other frameworks like Angular and Vue. We have alternatives to use react’s own hooks and contexts in addition to much appreciated react redux, mobx and more.

While react redux with thunks and sagas is widely adopted, the integration and amount of code it requires to function is overwhelming. And trying to integrate redux with typescript can be challenging and frustrating with all the extra steps required.

Having used VueX in Vue which provides a minimal and elegant approach based on redux, coming to react redux can be painful. That being said, I started looking for a simpler solution which lets you focus on application architecture rather than defining the complex flow for the library itself. So, I tried MobX and was impressed on how simple yet powerful the library is.

According to the official MobX docs, MobX is a battle tested library that makes state management simple and scalable by transparently applying functional reactive programming.

If you want to learn more about mobx-react, you might want to check this out.

MobX React Lite

This is a lighter version of mobx-react which supports React functional components only and as such makes the library slightly faster and smaller. As per my understanding that mobx-react is more well documented and widely used. Nevertheless, with the introduction of react hooks and updated context api, many of the mobX features are built right into react, and are rather performant and future proof. Hence, if you are starting a new project with react, I would suggest you try this lighter version instead.

Here are few implementation techniques for using MobX with react hooks. If you have used redux or react contexts, these should be fairly easier to understand.

store.ts

export function createStore() {
return {
data: [] as string[],
addData(item: string) {
this.data.push(item);
},
removeData(item: string){
this.data.splice(this.data.indexOf(item), 1)
},
};
}
export type TStore = ReturnType<typeof createStore>;

context.ts

import { useLocalStore } from 'mobx-react-lite';
import React from 'react';
import { createStore, TStore } from './store';
const StoreContext = React.createContext<TStore | null>(null);export const DataStoreProvider = ({ children }: any) => {
const store = useLocalStore(createStore);
return <StoreContext.Provider value={store}>{children}. </StoreContext.Provider>;
};
export const useDataStore = () => {
const store = React.useContext(StoreContext);
if (!store) {
throw new Error('useStore must be used within a StoreProvider.');
}
return store;
};

Note: useLocalStore turns a javascript literal into a store with observable properties. This is not needed if a store is created from a class object with observable attributes. Ref: https://tinyurl.com/y8mmnv8b

App.tsx

import React, {FC} from 'react';
import { DataStoreProvider } from 'store/context';
const App:FC = () => (
<DataStoreProvider>
<MyComponent />
</DataStoreProvider>
);

MyComponent.tsx

import React, { FC, ChangeEvent, useState } from "react";
import { useDataStore } from "./store/context";
import { observer } from 'mobx-react-lite';
const MyComponent: FC = observer(() => {
const [query, setQuery] = useState<string>("");
const store = useDataStore();
const { data, addData, removeData } = store;
const handleChange = (e: ChangeEvent<HTMLInputElement>):void => { setQuery(e.target.value);
};
return (
<div>
<div>
<input type="text" value={query} onChange={handleChange} />
<button onClick={() => addData(query)}>Add data</button>
</div>
<ul>
<button>Add data</button>

{data.map((value) => (
<li>
<span>{value}</span>
<button onClick={() => removeData(value)}>Remove data</button>
</li>
))}
</ul>
</div>
);
});
export default MyComponent;

We need to wrap our component with observer to keep track of changes in observables so that our component gets notified on state change and can re-render.

Usage with multiple stores

In a complex and scalable application, it is better to combine multiple stores for easy access and minimize code duplication for different stores.

dataStore.ts

import { action, decorate, observable } from 'mobx';class DataStore {
data: string[] = [];

addData(item: string) {
this.data.push(item);
},
removeData(item: string) {
this.data.splice(this.data.indexOf(item), 1);
}
}
decorate(DataStore, {
data: observable,
addData: action,
removeData: action
});
export default DataStore;

Note: We can also use decorators for observable and action. But decorator are experimental and not well supported in create-react-app.

themeStore.ts

import { action, decorate, observable } from 'mobx';class ThemeStore {
isDarkMode: boolean = false;

toggleDarkMode() {
this.isDarkMode = !this.isDarkMode;
},
}
decorate(DataStore, {
isDarkMode: observable,
toggleDarkMode: action
});
export default ThemeStore;

store.ts

import { createContext, useContext } from 'react';
import DataStore from './dataStore';
import ThemeStore from './themeStore'

export interface IStore {
dataStore: DataStore;
themeStore: ThemeStore;
}
export const store: IStore = {
dataStore: new DataStore(),
themeStore: new ThemeStore()
};
export const StoreContext = createContext(store);export const useStore = () => {
return useContext(StoreContext);
};

index.tsx

import { store, StoreContext } from './store';
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(
<StoreContext.Provider value={store}>
<App />
</StoreContext.Provider>,
document.getElementById('root')
);

MyComponent.tsx

import React, { FC, ChangeEvent, useState } from "react";
import { useStore } from "./store";
import { observer } from 'mobx-react-lite';
const MyComponent: FC = observer(() => {
const [query, setQuery] = useState<string>("");
const { dataStore } = useStore();
const handleChange = (e: ChangeEvent<HTMLInputElement>):void => {
setQuery(e.target.value);
};
return (
<div>
<div>
<input type="text" value={query} onChange={handleChange} />
<button onClick={() => dataStore.addData(query)}>Add data</button>
</div>
<ul>
<button>Add data</button>

{dataStore.data.map((value) => (
<li>
<span>{value}</span>
<button onClick={() => dataStore.removeData(value)}>Remove data</button>
</li>
))}
</ul>
</div>
);
});
export default MyComponent;

Simplify store architecture with MobX state tree

This library is a mobx powered state container widely popular in the mobx realm. It is important to note that it has its own type system that might be helpful for JS environments. But you cannot use TypeScript interfaces with its model which can be deal breaker if you are using TypeScript in your project.

dataStore.ts

import { types } from 'mobx-state-tree';const data = types.model({
items: types.array(types.string)
}).actions(self => ({
addData(item: string) {
self.data.push(item);
},
removeData(item: string) {
self.data.splice(this.data.indexOf(item), 1);
}
}));
export default data;

store.ts

import { Instance, onSnapshot, types } from 'mobx-state-tree';
import { createContext, useContext } from 'react';
import data from './dataStore';
const RootModel = types.model({
data
});
export const store = RootModel.create({
data: {
items: []
}
});
if (process.env.NODE_ENV !== 'production') {
onSnapshot(rootStore, snapshot => console.log('Snapshot: ', snapshot));
}
export type RootInstance = Instance<typeof RootModel>;const StoreContext = createContext<null | RootInstance>(null);export function useStore() {
const store = useContext(RootStoreContext);
if (store === null) {
throw new Error('Store cannot be null, please add a context provider');
}
return store;
}

index.tsx

import { store, StoreContext } from './store';
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(
<StoreContext.Provider value={store}>
<App />
</StoreContext.Provider>,
document.getElementById('root')
);

To conclude, mobx is a simple, minimal, tested and well written state management solution. I would love to hear your thoughts in the comments.

Thanks. Have a good day.

--

--