How to use Mobx in Next.js application (with demo)

Introduction

Hello, in these article i will create next.js application for showing books with mobx. I will provide:

  • SSR with mobx hydration
  • Using hydrated mobx on client side
  • Show possible mistakes

Disclaimer

Text

English is not my native language, but i hope the code will tell you more than text :D

DEMO

If you don't want to read welcome to sandbox :D - Demo

Short theory

Before you start, you need to understand exactly what order the server and client rendering takes place in next.js.
Here are some nuances about the order in which next.js works and in which order we expect hydration.

Designations

  • Component - page component (pages/index.jsx)
  • Next-server - next.js application host
  • _app - next.js app component (pages/_app.jsx)
  • API-server - backend-application (not provided in demo, but in real world will be)

Execution order in next

First, the server side props are called, then the component's _app is parsing, and only then the HTML page is rendered. Server pre-rendering has occurred. The client receives the statics and begins to deploy the react environment, starting with the _app of the application and ending with the component

You can check the order of execution in the demo, there is the logic for logging these steps to the console

The mobx state hydration scheme in next.js application is presented below

Let's code

Preparation

The structure of the project will be as follows:

|components - Folder with all app components (exclude pages)
| |-BoookCard
|pages (every folder is separate app route and page. Also the service components (_app, _document) are stored here)
| |-_app
| |-index (main page)
|store (mobx store)
| |-Books (BooksStore)
| |-index (useStore hook and initialize store methods)
|utils (mock datas and other utils)
| |-index

Defination stores

Let's init BooksStore (description in comments)

import { makeAutoObservable } from "mobx";
import { books, clientBooks } from "../utils";

class BooksStore {
  constructor() {
    // define and init observables
    this.books = [];
    this.searchParam = "";
    (make all object properties observables, getters computed, methods actions)
    makeAutoObservable(this);
  }

  setSearchParam = (param) => {
    this.searchParam = param;
  };

  setBooks = (books) => (this.books = books);

  get filteredBooks() {
    return this.books.filter((book) =>
      book.title.toLowerCase().includes(this.searchParam.toLowerCase())
    );
  }

  get totalBooks() {
    return this.books.length;
  }

  // if data is provided set this data to BooksStore 
  hydrate = (data) => {
    if (!data) return;
    this.setBooks(data.books);
  };

  // special method for demonstration
  fetchAndSetBooksOnClient = async () => {
    const newBooks = await Promise.resolve([...books, ...clientBooks]);
    console.log(newBooks);
    this.setBooks(newBooks);
  };
}

export default BooksStore

Note that on the client side we receive also Harry Potter books in fetch method. This is done to show the state of the store on the server side and on the client side.
We need to create a new store on every server request, and use one store on client side. Otherwise you will have problems with store
In next step we will provide store/index.js file:

// we need to enable static rendering for prevent rerender on server side and leaking memory
import { enableStaticRendering } from "mobx-react-lite";
import BooksStore from '../BooksStore'

// enable static rendering ONLY on server
enableStaticRendering(typeof window === "untdefined")

// init a client store that we will send to client (one store for client)
let clientStore

const initStore = (initData) => {
// check if we already declare store (client Store), otherwise create one
  const store = clientStore ?? new BooksStore();
// hydrate to store if receive initial data
  if (initData) store.hydrate(initData)

// Create a store on every server request
  if (typeof window === "undefined") return store
// Otherwise it's client, remember this store and return 
if (!clientStore) clientStore = store;
  return store
}

// Hoook for using store
export function useStore(initData) {
  return initStore(initData)
}

Connect with next.js

We need to create and provide _app component in pages directory. Let's do it

import { useStore } from "../store";
import { createContext } from "react";
import { getSide } from "../utils";

export const MobxContext = createContext();

const MyApp = (props) => {
  console.log("hello from _app - ", getSide());
  const { Component, pageProps, err } = props;
  const store = useStore(pageProps.initialState);
  return (
    <MobxContext.Provider value={store}>
      <Component {...pageProps} err={err} />
    </MobxContext.Provider>
  );
};

export default MyApp;

Example of fetching data on server

import { getSide, books } from "../utils";
import { useContext } from "react";
import { MobxContext } from "./_app";
import BookCard from "../components/BookCard";
import { observer } from "mobx-react-lite";

const IndexPage = () => {
  const {
    totalBooks,
    filteredBooks,
    setSearchParam,
    fetchAndSetBooksOnClient
  } = useContext(MobxContext);
  console.log("hello from Page component ", getSide());

  const handleOnInputChange = (e) => {
    setSearchParam(e.target.value);
  };

  return (
    <div>
      <h1>Books:</h1>
      <h3>TotalBooks: {totalBooks}</h3>
      <button onClick={fetchAndSetBooksOnClient}>Fetch on Client</button>
      <input placeholder="search" type="text" onChange={handleOnInputChange} />
      <hr />
      <div style={{ display: "flex" }}>
        {filteredBooks.map((book, index) => (
          <BookCard key={index} book={book} />
        ))}
      </div>
    </div>
  );
};

export const getServerSideProps = async () => {
  console.log("making server request before app", getSide());
  // here could be any async request for fetching data
  // const books = BooksAgent.getAll();
  return {
    props: {
      initialState: {
        booksStore: {
          books
        }
      }
    }
  };
};

export default observer(IndexPage);

And that's all. You can check mobx reactivity by adding new book (fetch on Client button) and searching book by title.

Possible mistakes

Mistakes:

  • Not create a new store on every server request (store/index:12) - on each request, the storage will be refilled with data
  • Forgot to do hydration (store/index:10) - non-compliance with content on server and client
  • Not using one store on client and server (MobxContext and useStore ONLY in _app component) - non-compliance with content on server and client

Links

I'm not the first to implement or explain this solution, I just tried to describe in more detail how it works and make an example more realistic
Offical github example
Another cool article

84