DEV Community

Ivan V.
Ivan V.

Posted on

Mobx Server Side Rendering with Next.js

In this article, we are going to use Mobx with the root store pattern and Next.js framework to do a server-side rendering of pages. If you haven't, please check out my article about Mobx root store pattern with React hooks.

The Project

The project is a simple counter that will start counting from 0. If a query parameter is passed in with a key of start, the server will use that query value and hydrate the counter to start counting from that value. Check it out here

Implementation

There are a couple of things that you need to watch out for when dealing with isomorphic applications. You must watch out for memory leaks, and you must take care not to mix up different users' data which can happen if you don't properly clean up your code after each server request. And there is also the process of hydration, you need to make sure to render the same context on the server and in the browser, when the page first loads or React will scream at you 😱.

Memory Leaks

Because of the way how Mobx handles dependency tracking, it can leak memory when running on the server. Luckily, Mobx has solved that issue a long time ago, and all you have to do is enable static rendering functionality for Mobx.

import { enableStaticRendering } from "mobx-react-lite";
// there is no window object on the server
enableStaticRendering(typeof window === "undefined");
Enter fullscreen mode Exit fullscreen mode

In the previous example, we have used enableStaticRendering function of the mobx-react-lite ( a special package that enables Mobx to be used with React) to enable static rendering whenever the window object is undefined, and since the window object only exists in the browser we enable static rendering only on the server.

// on the server
enableStaticRendering(true);

// in the browser
enableStaticRendering(false);
Enter fullscreen mode Exit fullscreen mode

And that all you have to do to make Mobx work on the server.

Always Fresh State

The second problem of potentially mixing the state of different requests can be solved by always creating a new Mobx store for each request (on the server) and when running in the browser, we create the store only once on the first load.

// file: src/providers/RootStoreProvider.tsx

// local module level variable - holds singleton store
let store: RootStore;

// function to initialize the store
function initializeStore():RootStore {
  const _store = store ?? new RootStore();

  // For server side rendering always create a new store
  if (typeof window === "undefined") return _store;

  // Create the store once in the client
  if (!store) store = _store;

  return _store;
}

Enter fullscreen mode Exit fullscreen mode

Function initializeStore() will be used by the React provider component to create the store and use it as a value:

export function RootStoreProvider({
  children,
}: {
  children: ReactNode;
}) {
  // create the store
  const store = initializeStore();

  return (
    <StoreContext.Provider value={store}>{children}</StoreContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

And that is all it takes to handle creating Mobx stores both on the server and in the browser.

Hydration

In order to show the initial HTML content on the page (before React actually runs) we need to render it on the server-side, and then use the same data from the server-side to render it on the client and make the application "alive". That process is called hydration.

Next.js framework solves the problem of hydration for React apps, and all it's left for us to do is to use that process with our Mobx stores:

First, we need to have a special method on our root store that we will call with the hydration data. Root store will then distribute that hydration data to all other stores.

export type RootStoreHydration = {
  childStoreOne?: CounterHydration;
};

export class RootStore {
  hydrate(data: RootStoreHydration) {
    // check if there is data for this particular store
    if(data.childStoreOne){
      this.childStoreOne.hydrate(data.childStoreOne);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

In the previous example, we have created the hydrate method on our root store that if there is hydration data it will get distributed to the child stores (which also have the hydrate method). Hydration data is a simple JSON serializable object with keys that map to child stores.

Now we need to change the initializeStore to accept the hydration data to be used when the root store is created.

function initializeStore(initialData?: RootStoreHydration): RootStore {
  const _store = store ?? new RootStore();

  // if there is data call the root store hydration method
  if (initialData) {
    _store.hydrate(initialData);
  }
  // For server side rendering always create a new store
  if (typeof window === "undefined") return _store;

  // Create the store once in the client
  if (!store) store = _store;

  return _store;
}
Enter fullscreen mode Exit fullscreen mode

The reason that the initialData parameter is optional is that when navigating to different pages, some pages might have no data to hydrate the store, so undefined will be passed in.

Next, we need to change the RootStoreProvider component to accept hydration data.

function RootStoreProvider({
  children,
  hydrationData,
}: {
  children: ReactNode;
  hydrationData?: RootStoreHydration;
}) {
  // pass the hydration data to the initialization function
  const store = initializeStore(hydrationData);

  return (
    <StoreContext.Provider value={store}>{children}</StoreContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

And finally, we need to add RootStoreProvider component to the application and pass the hydration data from the Next.js framework itself.
Since we are planning to use the stores throughout the whole application (React tree), the best place to do that is as close to the React tree root as possible, and in the case of the Next.js framework that would be the special App component. This App component is Next.js top-level component that is used to initialize all other pages.

function App({
  Component,
  pageProps,
}: {
  Component: NextPage;
  pageProps: any;
}) {
  return (
    <RootStoreProvider hydrationData={pageProps.hydrationData}>
      <Component {...pageProps} />;
    </RootStoreProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

And that's it, everything is connected, and Mobx stores will correctly run both on the server and in the browser.

Please note that in this article we have used one root store to wrap the whole application, but you could also have any number of other root stores (or single stores) that could wrap only certain pages. The process is exactly the same but your Provider components will live somewhere else in the React component tree.

repository: https://github.com/ivandotv/mobx-nextjs-root-store

demo: https://counter-demo.vercel.app

Discussion (10)

Collapse
greggcbs profile image
GreggHume

Thanks Ivan.

You are doing the following:

   // check if there is data for this particular store
    if(data.childStoreOne){
      this.childStoreOne.hydrate(data.childStoreOne);
    }
Enter fullscreen mode Exit fullscreen mode

This would mean the child store needs a hydrate method. What would that look like?

Collapse
ivandotv profile image
Ivan V. Author

Imagine the store is a todo list. If you were to load the data in the browser you would hit the nextjs API endpoint via fetch and load the todo's. However, since we are first rendering the data in the backend, we should get that data directly from the todo database, hydrate the store, so when the page is sent to the browser it already contains all the data, as a consequence if you try to load the page with javascript disabled, it will still show all the todos. As for the hydrate method, it should accept an object (an array of todo's) in this example, and to whatever it needs to do depending on the business logic. In this case, the hydrate method would probably have the same logic as the method that is calling the fetch when loading from the browser, that is because they would probably handle identical data.

Collapse
greggcbs profile image
GreggHume

Thanks, but theres some issues i have noticed with this approach for me.

1 . App bundle size and speed
With mobx it is not necessary to wrap an app with the storeprovider. I have noticed that if your stores get big this bloats the main app bundle size and penalizes on SEO performance. I shaved 40% of my page sizes by removing this.

  return (
    <RootStoreProvider hydrationData={pageProps.hydrationData}>
      <Component {...pageProps} />;
    </RootStoreProvider>
  );
Enter fullscreen mode Exit fullscreen mode

2 . I have noticed that using useRootStore() causes uneeded re-renders
Doing this will cause the component to rerun useRootStore every time an update happens. This is uneeded calling of useRootStore and in big apps will cause performance issues.

import {useRootStore} from "@/core/store/root.store";
function Component(){
    // component will rerun useRootStore everytime there is a change in data
    const {companyStore, listingStore} = useRootStore();

    return (
        <div>
            {companyStore.company.companyID}
        </div>
    )
}
export default observer(Component);
Enter fullscreen mode Exit fullscreen mode

VS

importing the store outside of the component which is way more performant

import {companyStore, listingStore} from "@/core/store/root.store";

function Component(){
   // component will only rerender parts of the dom thats data have changed
   // store will only be imported once because it is imported outside of the component
    return (
        <div>
            {companyStore.company.companyID}
        </div>
    )
}
export default observer(Component);
Enter fullscreen mode Exit fullscreen mode

3 . Hydration on the server?
Now im not sure how to hydrate on serverside using my above approaches but I would rather hydrate in the browser by passing in the data from serverside through props to the store than bloat my app by wrapping it with a provider and slowing it down using the hook. It is any case probable that the store has to run in the browser to initialise itself, and that will probably trigger a dom rerender - im not sure? So that computation is happening anyways which means the app is building twice - on serverside and in the client. Possibly, im not sure.

final thoughts
As I am on a drive for performance and battery life on devices - some of the points above have come to my attention. The above comments are not to degrade your solution but bring forward what I have discovered.

Thread Thread
ivandotv profile image
Ivan V. Author • Edited on
  1. You can use mobx without providers or hooks, just import the store, and reference it directly in the component.
    The reason I'm using the provider component and useRootStore hook is because of the testability of the components. Without them, how are you going to substitute (mock) the store when testing the components with something like react-testing-library?
    Root store is used in _app component, everything that is included in the _app component must be bundled for "first page load". If you don't need your store present through the whole app, don't include it in the root store, and load it only on the pages where it is needed, that will decrease the size.
    Question: suggested best practice for holding onto a store constructed with props #74

  2. I believe that you are wrong here because useRootStore can never trigger a render, all it does is return a stable reference to the mobx store (always the same object). The same applies for the root provider component, it will never trigger a render, the only purpose of the provider component and the hook is to enable easier testing (maybe I should have mentioned that in the article). In your example, how are you going to test your component in isolation?

  3. In nextjs, the application always renders twice once on the server, and then on the client, it has nothing to do with Mobx. getServerSideProps and getStaticProps are essentially hydration for react components and the principle is the same. You need to decide if you want to render an "empty" store in the browser when the page loads.
    If care about SEO, then you will render on the server, so the crawler has something to index.

All good questions, keep'em coming :)

Thread Thread
greggcbs profile image
GreggHume

1 . That is a good idea but then at some point its not clear what is available in the root context and what is not. But thats development.

2 . I am not doing testing so i didnt think of that, good point. To clarify this unnecesarry rerender discussion, I see you have done storeContext different to another example I was looking at and I got the two mixed up. To clarify your code anyways, does useContext know that you are passing an existing context in?

How much execution happens when this is run:

export 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

^ because from what I have seen, this function will run everytime a change happens in a component, if the component is wrapped with mobx observable. And if useContext does some processing when it is invoked then i am worried about this extra processing.

3 . I was not saying mobx causes both renders, i was saying that they happen anyways. Hydration happens anyways.

So if I do this, it has the same effect as what you were doing:

export async function getStaticProps(res) {
    return {
        props: {
            company: data[0].data,
            listingsByCategory: data[1].data.categories
        },
        revalidate: 300,
    }
}

export default function Company({company, listingsByCategory}) {
    companyStore.hydrate({company});
    listingStore.hydrate({listingsByCategory});

    return (
        <>
            <SomeComponentsThatUseTheStoreInternally />
        </>
    );
}
Enter fullscreen mode Exit fullscreen mode
Thread Thread
ivandotv profile image
Ivan V. Author • Edited on
  1. useRootStore hook is just a way to get to the root store without explicitly declaring the store inside the component, it's just getting back the value that is stored in context, and if there is no context above, it throws.

3.Yes exactly :) You just need to guard against calling the hydration more than once, currently if will hydrate every time the Company component is rendered.

Collapse
alejomartinez8 profile image
Alejandro Martinez

Thanks for the article! I have 2 questions? 1) When is best place to fetch the data to hydrate the components? On initializeStore or how can I access to store(in server) in getServerSideProps? 2) Is possible to persist the userStore for example to avoid auth fetch in each request, or is needed to refresh the store(in server)?

Collapse
ivandotv profile image
Ivan V. Author
  1. Depends what you want to do, if you have static rendering, than you need to hydrate the data on the server (when static page is built). You should not access the store in getServerSideProps rather you should just return data that will later be used do populate stores - passed to the root store provider by the page component.
  2. I'm not really sure what you mean.
Collapse
leandrit_ferizi profile image
Leandrit Ferizi

Thanks

Collapse
ninode97 profile image
ninode97

Amazing article, thank you.