Next.js with MobX

Next.js with MobX

By Mike Lewis

In this article I will go over using MobX with Next.js in a more practical manner than the with-mobx example in the Next.js repo. While the official example provides a good understanding of how to install and configure MobX, it does not do a good job of indicating how it can be practically used especially in the case of multiple stores and in a page with getInitialProps. This example will show you how to setup MobX in Next.js as well as how to hydrate the client side store with server side data.

Installation

First, install the required packages:

yarn add mobx mobx-react

or

npm install --save mobx mobx-react

Then install the babel plugins:

yarn add -D @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators

or

npm install --save-dev @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators

 

Next you will need to update or create a .babelrc file in the root directory with the following:

{
  "presets": [
    "next/babel"
  ],
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", { "loose": true }]
  ]
}

 

Creating Stores

Now we can create some stores. I put mine in a folder named stores/. For this example let's make two separate stores so that you can get the basics and expand from there. The first I'm calling UIStore.js and is really just used to control global UI elements. The second I'm calling PostStore.js and this is used to interact with an API to get posts.

stores/UIStore.js:


class UIStore {
  @observable searchOverlayOpen = false;

  @action setSearchOverlayOpen(value) {
    this.searchOverlayOpen = value;
  }
}

export default UIStore;

 

stores/PostStore.js:

import { observable, action } from 'mobx';

import { makeFetchRequest } from '../utils/API';

class PostStore {
  @observable post = null;

  endpoint = 'post';

  constructor(initialData = {}) {
    this.post = initialData.post;
  }

  async fetch(id) {
    const response = await makeFetchRequest(`${this.endpoint}/${id}/`);
    this.setPost(response);
  }

  @action setPost(post) {
    this.post = post;
  }
}

export default PostStore;

Now we need to setup a stores.js file in stores/ to contain these two MobX stores and initialize them. Note that in the case of UIStore, there will be no server side data to re-hydrate so it will be handled different than the PostStore which will need to be re-hyrated from the server. If we need to re-hydrate then we will have to pass in initialData from the server.

stores/stores.js:

import { useStaticRendering } from 'mobx-react';

import PostStore from './PostStore';
import UIStore from './UIStore';

const isServer = typeof window === 'undefined';
useStaticRendering(isServer);

let store = null;

export default function initializeStore(initialData = { postStore: {} }) {
  if (isServer) {
    return {
      postStore: new PostStore(initialData.postStore),
      uiStore: new UIStore(),
    };
  }
  if (store === null) {
    store = {
      postStore: new PostStore(initialData.postStore),
      uiStore: new UIStore(),
    };
  }

  return store;
}

 

Wiring it Up

Now we will need to initialize stores and wrap our app in a MobX Provider in our custom pages/_app.js file.

pages/_app.js:

import React from 'react';
import App, { Container } from 'next/app';
import { Provider } from 'mobx-react';

import initializeStore from '../stores/stores';

class CustomApp extends App {
  static async getInitialProps(appContext) {
    const mobxStore = initializeStore();
    appContext.ctx.mobxStore = mobxStore;
    const appProps = await App.getInitialProps(appContext);
    return {
      ...appProps,
      initialMobxState: mobxStore,
    };
  }

  constructor(props) {
    super(props);
    const isServer = typeof window === 'undefined';
    this.mobxStore = isServer ? props.initialMobxState : initializeStore(props.initialMobxState);
  }

  render() {
    const { Component, pageProps } = this.props;
    return (
      <Provider {...this.mobxStore}>
        <Container>
          <Component {...pageProps} />
        </Container>
      </Provider>
    );
  }
}

export default CustomApp;

 

Using It

The part that I had trouble finding an explanation for is how to actually use a MobX store in a pages' getInitialProps function while hydrating from server data. Here is how you do that, roughly:

import React, { Component } from 'react';
import { inject, observer } from 'mobx-react';

@inject('postStore') @observer
class Post extends Component {
  static async getInitialProps({ mobxStore, query }) {
    await mobxStore.postStore.fetch(query.id);
    return { post: mobxStore.postStore.post };
  }

  render() {
    const { post } = this.props;
    return (
      <div>
        <h1>{post.title}</h1>
      </div>
    );
  }
}

export default Post;

Above I am also using next-routes and am passing in a query variable to getInitialProps with the id of the post. You can omit that if you don't require a query paramater. Basically, getInitialProps will receive mobxStore as a keyword argument. mobxStore is an object containing all of your stores that you passed in via pages/_app.js.

That should get you started but feel free to reach out if you have any issues.