js

How to Combine Next.js and MobX for Fast, Reactive Web Apps

Learn how to build SEO-friendly, server-rendered pages with instant client-side interactivity using Next.js and MobX.

How to Combine Next.js and MobX for Fast, Reactive Web Apps

I’ve been building web applications for years, and a persistent challenge keeps resurfacing: how do you make a page that loads fast for search engines and users, but also feels instantly responsive and alive when someone clicks a button? I found myself constantly choosing between server-side rendering for performance and complex client-side state libraries for interactivity. It felt like picking one benefit meant sacrificing the other. This friction led me to combine two powerful tools: Next.js for rendering and MobX for state.

The goal is simple. We want a page that is fully formed when it arrives in the browser, complete with all its data, ready to be indexed by Google. But the moment a user interacts with it, we want that page to react instantly, without waiting for new server requests, updating only the parts that need to change. This is where the marriage of Next.js and MobX becomes so compelling.

Think about a dashboard. During a server-side render with Next.js, we can fetch all the initial chart data, user info, and metrics. The HTML sent to the browser is complete. Now, what if the user changes a date filter? With a basic setup, you might trigger a new server request, re-render the whole page, and create a jarring experience. But what if the data was already reactive? What if changing a filter just automatically updated the relevant chart, as if by magic?

This is MobX’s strength. It uses a concept of observables. You declare your data as “observable,” and any component that uses that data automatically “observes” it. When the data changes, only those specific components update. There’s no need to manually pass callbacks or dispatch actions through multiple levels of your app. You change the data, and the view updates. It’s a wonderfully direct way to think about state.

Let’s look at a basic store. Imagine we’re building an e-commerce app with a shopping cart.

// stores/CartStore.js
import { makeAutoObservable } from 'mobx';

class CartStore {
  items = [];
  total = 0;

  constructor() {
    makeAutoObservable(this);
  }

  addItem(product) {
    const existingItem = this.items.find(item => item.id === product.id);
    if (existingItem) {
      existingItem.quantity += 1;
    } else {
      this.items.push({ ...product, quantity: 1 });
    }
    this.calculateTotal();
  }

  calculateTotal() {
    this.total = this.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
  }
}

export const cartStore = new CartStore();

Now, here’s the crucial part for Next.js. We cannot simply import this store and use it. Why? Because Next.js renders pages on the server (Node.js) and then again on the client (the browser). If we use a single instance, we’ll end up with different stores between the server and client, causing mismatches and errors.

The solution is to create a fresh store for each request on the server and ensure the same initial state is used to “hydrate” the store on the client. We use Next.js’s data fetching functions, like getServerSideProps, to prepare this initial state.

// pages/index.js
import { observer } from 'mobx-react-lite';
import { cartStore } from '../stores/CartStore';

// A simple product component that observes the store
const ProductItem = observer(({ product }) => {
  return (
    <div>
      <h3>{product.name}</h3>
      <p>Price: ${product.price}</p>
      <button onClick={() => cartStore.addItem(product)}>
        Add to Cart
      </button>
    </div>
  );
});

// Our main page component
const HomePage = observer(({ initialCartData }) => {
  // In a real app, we would use a context/provider to inject the store
  // This is a simplified example
  return (
    <div>
      <h1>Products</h1>
      <ProductItem product={{ id: 1, name: 'T-Shirt', price: 19.99 }} />
      <div>
        <h2>Cart</h2>
        <p>Items: {cartStore.items.length}</p>
        <p>Total: ${cartStore.total}</p>
      </div>
    </div>
  );
});

// This runs on the server for each request
export async function getServerSideProps() {
  // Here you might fetch products from an API
  // For the cart, we initialize the store with any existing data (e.g., from a session)
  // const initialCartData = await fetchCartFromSession();

  // For this example, we start with an empty cart.
  // The key is that any initial state must be serializable.
  const initialCartData = { items: [], total: 0 };

  return {
    props: {
      initialCartData, // This will be passed to the page component
    },
  };
}

export default HomePage;

The missing piece is connecting the server-provided initial state to the client-side store. We need a mechanism to create a new store instance for each page request on the server and reuse that same initialized store on the client. This often involves using React Context and a custom hook.

// lib/store-context.js
import { createContext, useContext, useEffect } from 'react';
import { CartStore } from '../stores/CartStore';

const StoreContext = createContext(undefined);

export const StoreProvider = ({ children, initialState }) => {
  // Create a store instance only once per client-side navigation
  const store = new CartStore();
  
  // If initial state is provided (from getServerSideProps), apply it to the store
  useEffect(() => {
    if (initialState) {
      // A method to hydrate the store safely
      store.hydrate(initialState);
    }
  }, [initialState, store]);

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

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

Then, you would wrap your application in _app.js with this StoreProvider, passing the pageProps.initialCartData to it. This pattern ensures consistency.

The beauty of this setup is its efficiency. MobX’s reactivity system is fine-grained. When cartStore.total updates after addItem is called, only the component displaying the total re-renders. The product list doesn’t. This keeps your application fast even as state becomes complex.

So, is this combination the right choice for every project? Not always. For very simple state, React’s own useState and useContext might be enough. For applications requiring strict, traceable state transitions with middleware, other libraries might be preferable. But for many applications—dashboards, admin panels, interactive tools—this pairing offers a fantastic balance. You get the SEO and performance benefits of server-side rendering with the fluid, responsive feel of a single-page application.

The development experience is also a major win. Writing code that feels straightforward—changing data and seeing the UI update—reduces cognitive load. It allows you to focus more on building features and less on managing state update logistics.

Have you ever built an app that felt sluggish after the initial load, or struggled with state mismatches between server and client? This integration directly addresses those pain points. It provides a clear path from a static, fast-loading page to a dynamic, reactive application.

I encourage you to try this pattern in your next Next.js project. Start with a simple store, connect it using the provider pattern, and experience how it simplifies data flow. If you’ve found other ways to tackle this server-client state challenge, I’d love to hear about them. Share your thoughts in the comments below, and if this guide helped clarify the path, feel free to pass it along to others who might be facing the same puzzle.


As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!


📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!


Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Keywords: nextjs, mobx, server side rendering, react state management, web performance



Similar Posts
Blog Image
Complete Guide to Next.js Prisma Integration: Build Type-Safe Full-Stack Applications in 2024

Learn how to integrate Next.js with Prisma ORM for type-safe, full-stack web applications. Build powerful data-driven apps with seamless database operations.

Blog Image
Complete Guide to Integrating Next.js with Prisma ORM for Type-Safe Full-Stack Development

Learn how to integrate Next.js with Prisma ORM for type-safe database access, seamless API development, and enhanced full-stack app performance. Start building today!

Blog Image
Building Event-Driven Microservices: Complete NestJS, RabbitMQ, and MongoDB Production Guide

Learn to build scalable event-driven microservices with NestJS, RabbitMQ & MongoDB. Master CQRS, event sourcing & distributed systems. Complete tutorial.

Blog Image
Complete Guide to Integrating Next.js with Prisma ORM for Type-Safe Full-Stack Development

Learn how to integrate Next.js with Prisma ORM for type-safe, full-stack web applications. Build scalable apps with seamless database operations today.

Blog Image
How Vitest Transformed My Testing Workflow with Vite

Discover how integrating Vitest with Vite simplifies testing, speeds up feedback loops, and eliminates config headaches.

Blog Image
Zustand vs React Query: Smarter State Management for Modern React Apps

Learn when to use Zustand for client state and React Query for server state in React apps to build cleaner, scalable frontends.