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

    115

    This website collects cookies to deliver better user experience

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