Mobx Root Store Pattern with React Hooks
Ivan V.
Posted on November 30, 2020
In this article, we are going to use the Mobx state library and the root store pattern to organize multiple stores in React application then, we are going to use the React provider component and React hooks to pull in those stores into the components. At the end of the article, I'm going to share a repository with the demo project that implements all these concepts.
Why Mobx
From the docs:
MobX is unopinionated and allows you to manage your application state outside of any UI framework. This makes your code decoupled, portable, and above all, easily testable.
By separating your business logic outside of any web framework, you then use the framework only as a simple view to reflect your application state. Mobx has support for (Angular, Vuejs, React Native, Dart, etc..). Also, a big selling point of Mobx, is that you can work on your business logic before ever touching a line of React code.
It is such a great library honestly, ever since I've found it I have never used anything else to manage state in React. If you are a complete beginner, I encourage you to take a look at the excellent Mobx documentation to learn the basics and then come back to this article.
Root Store Pattern
Root store pattern is a simple pattern that the Mobx community started to use whenever there were multiple Mobx stores (which are just classes, or plain objects) that need to communicate with each other. This is accomplished by creating one class (or object) that will hold all other classes (or objects). Stores that are contained by the root store will also hold a reference to the root store so that they can essentially reference any other store contained by the root store.
class RootStore {
childStoreOne: ChildStoreOne
childStoreTwo: ChildStoreTwo
constructor() {
this.childStoreOne = new ChildStoreOne(this)
this.childStoreTwo = new ChildStoreTwo(this)
}
}
class ChildStoreOne {
root: RootStore
constructor(root: RootStore) {
this.root = root
}
methodOne() {}
}
class ChildStoreTwo {
root: RootStore
constructor(root: RootStore) {
this.root = root
}
getSomethingFromStoreOne() {
this.root.childStoreOne.methodOne()
}
}
And that is pretty much all there is regarding the root store pattern.
It's a common practice to use a root
store only as a bucket that contains all other stores, it shouldn't have any other responsibilities, and it should most likely be a singleton.
One Caveat
There is one caveat with the root store pattern, and it might not apply to your code depending on what you are trying to do.
Notice how inside the root store we are constructing store one
, then store two
? When the first store is instantiated the second store doesn't exist. That means that we can't access the second store in the first store constructor function.
class ChildStoreOne {
root: RootStore
constructor(root: RootStore) {
this.root = root
this.root.childStoreTwo // error - doesn't exist yet
}
}
In order to solve this, there are two solutions:
- Never access other stores in the constructor (constructor functions shouldn't do any real work anyway).
- Create an initialization method on the child's classes that will do the real work that needs to be done when instantiating the class instance.
Method 2:
class RootStore {
childStoreOne: ChildStoreOne
childStoreTwo: ChildStoreTwo
constructor() {
this.childStoreOne = new ChildStoreOne(this)
this.childStoreTwo = new ChildStoreTwo(this)
// call init method on all child classes
// use a loop if there are to many classes
this.childStoreOne.init()
this.childStoreTwo.init()
}
}
class ChildStoreOne {
root: RootStore
storeTwo: ChildStoreTwo
constructor(root: RootStore) {
this.root = root
// no work here only assignments
}
init() {
// safe to access other stores
this.root.childStoreTwo.doSomething()
}
}
class ChildStoreTwo {
root: RootStore
storeOne: ChildStoreOne
constructor(root: RootStore) {
this.root = root
// move real initialization work to the init method
}
init() {
// safe to access other stores
this.root.childStoreOne.doSomething()
}
}
We are done with the store pattern, but before we move on to the React setup, I would just like to point out that in the previous examples, we created two child stores via ES6 classes however, we could have also used plain objects. In that case, we need to create a function that will accept a root store as an argument and return a plain object that will represent a child store.
function createChildStoreTwo(root: RootStore) {
return {
root,
getSomethingFromStoreOne() {
this.root.childStoreOne.doSomething()
},
}
}
React Setup
React implementation is very simple, and it can be done in three steps.
- Create a context.
- Create a provider function component.
- Create a hook for using the store inside the components.
// holds a reference to the store (singleton)
let store: RootStore
// create the context
const StoreContext = createContext<RootStore | undefined>(undefined);
// create the provider component
function RootStoreProvider({ children }: { children: ReactNode }) {
//only create the store once ( store is a singleton)
const root = store ?? new RootStore()
return <StoreContext.Provider value={root}>{children}</StoreContext.Provider>
}
// create the hook
function useRootStore() {
const context = useContext(StoreContext)
if (context === undefined) {
throw new Error("useRootStore must be used within RootStoreProvider")
}
return context
}
Next, we are going to wrap the whole application with the RootStoreProvider
component now, this might be strange if you never used Mobx before and you are thinking "Wait are we going to render the whole application from the root every time something in the store (provider) changes?". Wrong, this is not how Mobx works.
From the docs:
Effortless optimal rendering
All changes to and uses of your data are tracked at runtime, building a dependency tree that captures all relations between state and output. This guarantees that computations depending on your state, like React components, run only when strictly needed. There is no need to manually optimize components with error-prone and sub-optimal techniques like memoization and selectors.
Basically, that means that the components will render only when the properties of the store that are used directly inside the component are changed. For example, if the store has an object which holds name
and lastName
and the component only uses the name
property {store.name}
and the lastName
changes, the component will not render, since it doesn't use the lastName
property.
So we wrap the whole application:
ReactDOM.render(
<React.StrictMode>
<RootStoreProvider>
<App />
</RootStoreProvider>
</React.StrictMode>,
document.getElementById("root")
);
Now, for using Mobx powered stores inside the components, we need to wrap every React functional component with the Mobx observer
function. When we do that, Mobx will make sure that the component is rendered every time some property of the store is changed and it is also accessed in the component itself. If you are wondering if you can still use React state hooks useState
, useReducer
, useEffect
inside the component, yes you can and the component will behave normally.
import { observer } from "mobx-react-lite";
export const MyComponent = observer(function MyComponent() {
const store = useRootStore();
return (
<div>
{store.childStoreOne.name} // only render when the name changes
</div>
)
})
Bonus
You can also destructure the store from the useRootStore()
hook like this:
const { childStoreOne } = useRootStore()
Or you can create additional hooks that will only return specific child stores:
// return only childStoreOne
function useChildStoreOne() {
const { childStoreOne } = useRootStore()
return childStoreOne
}
And that's it, that's how simple it is to use Mobx root store pattern with React hooks. If you want to learn more about Mobx and React integration, there is a special section dedicated to React in the docs
As promised, I'm going to share a repository for a small demo that uses the root store pattern to create a simple clock that can be paused and resumed.
You can check it out at: https://clock-demo.netlify.app/
Repository: https://github.com/ivandotv/react-hooks-mobx-root-store
Please note that in this article I've skipped some Mobx initialization code in order not to detract from the essence of the article which is the pattern and React integration. In the demo repository, there is a fully functional example.
Stay tuned for part 2 when we are going to do server-side rendering with Mobx and Next.js.
Posted on November 30, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.