← Back to DevBytes

Server-Side Rendering with MobX: SSR, SSG, ISR

Introduction to Server-Side Rendering with MobX

State management is a cornerstone of modern React applications, and MobX is one of the most popular choices due to its simplicity and reactive programming model. However, when moving from a purely client-side rendered (CSR) application to a server-rendered architecture, developers face unique challenges. Server-Side Rendering (SSR), Static Site Generation (SSG), and Incremental Static Regeneration (ISR) require careful handling of state to ensure that the server and client remain in sync.

What is SSR, SSG, and ISR?

Why Does It Matter for MobX?

In a standard client-side React app, MobX stores are instantiated once and live for the duration of the user's session. In SSR, the server handles multiple requests simultaneously. If you use a global singleton MobX store on the server, you risk cross-request state pollution, where User A's data leaks into User B's request. Furthermore, you must serialize the server's MobX state and pass it to the client so the client can "hydrate" the application without flickering or making duplicate API calls.

Setting Up MobX for Next.js

To demonstrate SSR, SSG, and ISR with MobX, we will use Next.js, the leading React framework for server rendering. First, we need to create a MobX store architecture that safely handles both server and client environments.

Creating the MobX Store

We will create a store that can be initialized with default data or pre-fetched data. This is crucial for hydration.

// lib/store.js
import { makeAutoObservable } from 'mobx';

class UserStore {
  users = [];

  constructor(initialData = {}) {
    makeAutoObservable(this);
    if (initialData.users) {
      this.users = initialData.users;
    }
  }

  addUser(user) {
    this.users.push(user);
  }

  clearUsers() {
    this.users = [];
  }
}

class RootStore {
  userStore;

  constructor(initialData = {}) {
    this.userStore = new UserStore(initialData.userStore);
  }
}

let clientStore = null;

// This function ensures we only create a singleton on the client,
// but always create a fresh instance on the server.
export function initializeStore(initialData = null) {
  const _store = new RootStore(initialData);
  
  // If we are on the server, always return a fresh store
  if (typeof window === 'undefined') {
    return _store;
  }

  // If we are on the client and haven't created a store yet, create one
  if (!clientStore) {
    clientStore = _store;
  }

  return clientStore;
}

Hydrating State on the Client

Next, we need a custom App component to provide the store to our React component tree using React Context.

// pages/_app.js
import { createContext, useContext } from 'react';
import { initializeStore } from '../lib/store';

const StoreContext = createContext();
let store;

export function useStore() {
  const context = useContext(StoreContext);
  if (!context) {
    throw new Error('useStore must be used within StoreProvider');
  }
  return context;
}

export function StoreProvider({ children, initialState }) {
  store = initializeStore(initialState);
  return (
    <StoreContext.Provider value={store}>
      {children}
    </StoreContext.Provider>
  );
}

function MyApp({ Component, pageProps }) {
  return (
    <StoreProvider initialState={pageProps.initialState}>
      <Component {...pageProps} />
    </StoreProvider>
  );
}

export default MyApp;

Implementing SSR, SSG, and ISR

Now that our store is set up to handle hydration safely, we can implement the three rendering strategies. In all cases, the pattern is the same: fetch data on the server, initialize the store with that data, serialize the store to plain JSON, and pass it down as props.

Server-Side Rendering (SSR)

For SSR, we use getServerSideProps. This function runs on every request. We fetch the data, populate the store, and extract the serializable state.

// pages/ssr.js
import { useStore } from '../_app';
import { initializeStore } from '../lib/store';
import { observer } from 'mobx-react-lite';

export async function getServerSideProps() {
  // 1. Fetch data from an API or database
  const res = await fetch('https://jsonplaceholder.typicode.com/users');
  const users = await res.json();

  // 2. Initialize a fresh store instance with the fetched data
  const mobxStore = initializeStore({
    userStore: {
      users: users,
    },
  });

  // 3. Serialize the store to pass it as props
  return {
    props: {
      initialState: {
        userStore: {
          users: mobxStore.userStore.users,
        },
      },
    },
  };
}

const SSRPage = observer(() => {
  const store = useStore();
  
  return (
    <div>
      <h1>SSR with MobX</h1>
      <ul>
        {store.userStore.users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
});

export default SSRPage;

Static Site Generation (SSG)

For SSG, we use getStaticProps. This runs once at build time. The code is almost identical to SSR, but the execution context and caching behavior are different.

// pages/ssg.js
import { useStore } from '../_app';
import { initializeStore } from '../lib/store';
import { observer } from 'mobx-react-lite';

export async function getStaticProps() {
  const res = await fetch('https://jsonplaceholder.typicode.com/users');
  const users = await res.json();

  const mobxStore = initializeStore({
    userStore: {
      users: users,
    },
  });

  return {
    props: {
      initialState: {
        userStore: {
          users: mobxStore.userStore.users,
        },
      },
    },
  };
}

const SSGPage = observer(() => {
  const store = useStore();
  
  return (
    <div>
      <h1>SSG with MobX</h1>
      <ul>
        {store.userStore.users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
});

export default SSGPage;

Incremental Static Regeneration (ISR)

ISR builds upon SSG by adding a revalidate property to the returned props object. This tells Next.js to regenerate the page in the background if a request comes in after the specified number of seconds.

// pages/isr.js
import { useStore } from '../_app';
import { initializeStore } from '../lib/store';
import { observer } from 'mobx-react-lite';

export async function getStaticProps() {
  const res = await fetch('https://jsonplaceholder.typicode.com/users');
  const users = await res.json();

  const mobxStore = initializeStore({
    userStore: {
      users: users,
    },
  });

  return {
    props: {
      initialState: {
        userStore: {
          users: mobxStore.userStore.users,
        },
      },
      // Re-generate the page at most once every 60 seconds
      revalidate: 60, 
    },
  };
}

const ISRPage = observer(() => {
  const store = useStore();
  
  return (
    <div>
      <h1>ISR with MobX</h1>
      <p>This page is statically generated but updates every 60 seconds.</p>
      <ul>
        {store.userStore.users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
});

export default ISRPage;

Best Practices for MobX and SSR

Conclusion

Integrating MobX with Server-Side Rendering, Static Site Generation, and Incremental Static Regeneration unlocks powerful capabilities for building fast, SEO-friendly React applications. By understanding the lifecycle of server rendering and strictly adhering to the pattern of fetching data, initializing a request-isolated store, and serializing the state for client hydration, you can leverage MobX's reactive state management without sacrificing the performance benefits of modern rendering strategies. Always remember to keep your server state isolated per request and serialize your data carefully to maintain a seamless experience between the server and the browser.

— Ad —

Google AdSense will appear here after approval

← Back to all articles