DEV Community

Ivan V.
Ivan V.

Posted on

Mobx Root Store Pattern with React Hooks

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()
  }
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

In order to solve this, there are two solutions:

  1. Never access other stores in the constructor (constructor functions shouldn't do any real work anyway).
  2. 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()
  }
}

Enter fullscreen mode Exit fullscreen mode

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()
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

React Setup

React implementation is very simple, and it can be done in three steps.

  1. Create a context.
  2. Create a provider function component.
  3. 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
}
Enter fullscreen mode Exit fullscreen mode

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")
);

Enter fullscreen mode Exit fullscreen mode

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>
  )
})
Enter fullscreen mode Exit fullscreen mode

Bonus

You can also destructure the store from the useRootStore() hook like this:

const { childStoreOne } = useRootStore()
Enter fullscreen mode Exit fullscreen mode

Or you can create additional hooks that will only return specific child stores:

// return only childStoreOne
function useChildStoreOne() {
  const { childStoreOne } = useRootStore()
  return childStoreOne
}
Enter fullscreen mode Exit fullscreen mode

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.
application screenshot
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.

Top comments (3)

Collapse
 
tonycaputo profile image
Antonio Caputo

Hi Ivan, just a question, does this pattern promotes a sort of circular dependencies between the stores?
I just made a simple codesandbox and when I try to see variables inside the store It shows a continuous nesting of class properties.
Like rootstore.chilstorone.rootstore.childstoreone and so on.
Isn't that make debugging uncomfortable ?
Cheers.

Collapse
 
ivandotv profile image
Ivan V.

This pattern is safe if that is what you are asking. Yes the stores are referencing each other, but there are no memory leaks. You could also create a special class (service) that would handle out the references to other stores service.getStoreA() or service.getStore('A') and pass that class to child stores instead of the root store.

Collapse
 
shivamfinzome profile image
Shivam

can you provide an example for these?

Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more