Skip to main content

Next.js MobX

Introduction

MobX is a battle-tested state management library that makes state management simple and scalable by transparently applying functional reactive programming principles. When combined with Next.js, MobX provides a powerful solution for managing application state across server-rendered and client-side components.

In this guide, we'll explore how to integrate MobX with Next.js, understand its core concepts, and build practical examples to demonstrate effective state management patterns.

What is MobX?

MobX is a state management solution that uses observable patterns to automatically track state changes and update your UI accordingly. It works on three simple principles:

  1. Observable state - State that can be tracked for changes
  2. Computed values - Values derived from observable state
  3. Reactions - Side effects that run when observed state changes

Unlike Redux's unidirectional data flow, MobX allows for a more flexible approach where components can directly modify the state when needed, making it simpler for beginners to understand and implement.

Setting Up MobX in Next.js

Step 1: Install Dependencies

First, let's install the necessary packages:

bash
npm install mobx mobx-react-lite
# or
yarn add mobx mobx-react-lite
  • mobx: The core library
  • mobx-react-lite: Provides React bindings for functional components

Step 2: Create a Store

Let's create a simple counter store to understand the basics:

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

class CounterStore {
count = 0;

constructor() {
makeAutoObservable(this);
}

increment() {
this.count += 1;
}

decrement() {
this.count -= 1;
}

reset() {
this.count = 0;
}

// Computed value example
get doubleCount() {
return this.count * 2;
}
}

export default new CounterStore();

Step 3: Create a Store Context for Next.js

To make our store available throughout the Next.js application, we'll create a store context:

jsx
// stores/storeContext.js
import React, { createContext, useContext } from 'react';
import counterStore from './counterStore';

// Create a store container with all stores
const store = {
counterStore
};

// Create context
const StoreContext = createContext(store);

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

// Hook to use the store in components
export const useStore = () => useContext(StoreContext);

Step 4: Add the Provider to Your Next.js App

Now, let's wrap our Next.js application with the MobX store provider:

jsx
// pages/_app.js
import { StoreProvider } from '../stores/storeContext';

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

export default MyApp;

Using MobX in Next.js Components

Basic Counter Example

Now that our store is set up, let's use it in a component:

jsx
// pages/counter.js
import { observer } from 'mobx-react-lite';
import { useStore } from '../stores/storeContext';

const CounterPage = observer(() => {
const { counterStore } = useStore();

return (
<div className="counter-container">
<h1>MobX Counter Example</h1>

<div className="counter">
<h2>Count: {counterStore.count}</h2>
<p>Double Count: {counterStore.doubleCount}</p>
</div>

<div className="buttons">
<button onClick={() => counterStore.increment()}>Increment</button>
<button onClick={() => counterStore.decrement()}>Decrement</button>
<button onClick={() => counterStore.reset()}>Reset</button>
</div>
</div>
);
});

export default CounterPage;

Notice the observer wrapper around our component. This is crucial as it makes the component react to changes in the observable state automatically.

Key MobX Concepts in Next.js

1. Observables

Observables are the state properties that MobX will track changes for. In our counter example, count is an observable.

jsx
// Creating more observables
import { makeAutoObservable } from 'mobx';

class UserStore {
user = {
name: '',
email: '',
isLoggedIn: false
};

constructor() {
makeAutoObservable(this);
}

setUser(userData) {
this.user = { ...userData };
}

logout() {
this.user = {
name: '',
email: '',
isLoggedIn: false
};
}
}

2. Actions

Actions are methods that modify the state. In MobX, they're simply class methods in your store:

jsx
// Example action to update multiple properties
updateUserProfile(name, email) {
this.user.name = name;
this.user.email = email;
}

3. Computed Values

Computed values are derived from your state but are cached and only recalculated when needed:

jsx
// Adding a computed value
get userName() {
return this.user.name || 'Guest';
}

4. Reactions

Reactions are side effects that automatically run when observables they use are changed. In React components, this happens automatically when using the observer HOC.

Advanced MobX with Next.js

Handling Server-Side Rendering

MobX needs special handling for server-side rendering in Next.js. Let's create a more sophisticated setup:

jsx
// stores/createStore.js
import { enableStaticRendering } from 'mobx-react-lite';
import CounterStore from './counterStore';
import UserStore from './userStore';

// Enable static rendering for SSR
const isServer = typeof window === 'undefined';
enableStaticRendering(isServer);

// Store factory to ensure each request gets a new store instance
function createStore() {
return {
counterStore: new CounterStore(),
userStore: new UserStore()
};
}

// Singleton store for client-side
let clientSideStore;

// Function to initialize store on client or server
export function initializeStore(initialData = null) {
// For SSR, always create a new store
if (isServer) {
return createStore();
}

// Create the store once in the client
if (!clientSideStore) {
clientSideStore = createStore();
}

// Hydrate store with initialData if needed
if (initialData) {
// Update store with initial data
if (initialData.counterStore) {
clientSideStore.counterStore.count = initialData.counterStore.count;
}
// Add more hydration as needed
}

return clientSideStore;
}

Now update the provider:

jsx
// stores/storeContext.js
import React, { createContext, useContext } from 'react';
import { initializeStore } from './createStore';

const StoreContext = createContext(null);

export const StoreProvider = ({ children, initialState }) => {
const store = initializeStore(initialState);

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

export const useStore = () => useContext(StoreContext);

And update your _app.js:

jsx
// pages/_app.js
import { StoreProvider } from '../stores/storeContext';

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

export default MyApp;

Hydrating from Server State

For pages that need hydration from server data:

jsx
// pages/hydrated-counter.js
import { observer } from 'mobx-react-lite';
import { useStore } from '../stores/storeContext';
import { initializeStore } from '../stores/createStore';

const HydratedCounterPage = observer(() => {
const { counterStore } = useStore();

return (
<div>
<h1>Server Hydrated Counter</h1>
<p>Count: {counterStore.count}</p>
<button onClick={() => counterStore.increment()}>Increment</button>
</div>
);
});

// Get initial props from server
export async function getServerSideProps() {
// Create a store instance on the server
const store = initializeStore();

// Set initial value (could come from a database, API, etc.)
store.counterStore.count = 100;

// Return the serialized state
return {
props: {
initialState: {
counterStore: {
count: store.counterStore.count
}
}
}
};
}

export default HydratedCounterPage;

Practical Example: Todo List Application

Let's create a more complete example with a todo list application:

Todo Store

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

class TodoStore {
todos = [];
filter = 'all'; // all, active, completed

constructor() {
makeAutoObservable(this);
}

// Actions
addTodo(text) {
this.todos.push({
id: Date.now(),
text,
completed: false
});
}

toggleTodo(id) {
const todo = this.todos.find(todo => todo.id === id);
if (todo) {
todo.completed = !todo.completed;
}
}

removeTodo(id) {
this.todos = this.todos.filter(todo => todo.id !== id);
}

setFilter(filter) {
this.filter = filter;
}

// Computed values
get filteredTodos() {
switch (this.filter) {
case 'active':
return this.todos.filter(todo => !todo.completed);
case 'completed':
return this.todos.filter(todo => todo.completed);
default:
return this.todos;
}
}

get completedCount() {
return this.todos.filter(todo => todo.completed).length;
}

get activeCount() {
return this.todos.length - this.completedCount;
}
}

export default TodoStore;

Todo List Component

jsx
// pages/todo.js
import { useState } from 'react';
import { observer } from 'mobx-react-lite';
import { useStore } from '../stores/storeContext';

const TodoPage = observer(() => {
const { todoStore } = useStore();
const [newTodo, setNewTodo] = useState('');

const handleAddTodo = (e) => {
e.preventDefault();
if (newTodo.trim()) {
todoStore.addTodo(newTodo);
setNewTodo('');
}
};

return (
<div className="todo-container">
<h1>MobX Todo List</h1>

<form onSubmit={handleAddTodo}>
<input
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="What needs to be done?"
/>
<button type="submit">Add Todo</button>
</form>

<div className="filters">
<button
className={todoStore.filter === 'all' ? 'active' : ''}
onClick={() => todoStore.setFilter('all')}
>
All ({todoStore.todos.length})
</button>
<button
className={todoStore.filter === 'active' ? 'active' : ''}
onClick={() => todoStore.setFilter('active')}
>
Active ({todoStore.activeCount})
</button>
<button
className={todoStore.filter === 'completed' ? 'active' : ''}
onClick={() => todoStore.setFilter('completed')}
>
Completed ({todoStore.completedCount})
</button>
</div>

<ul className="todo-list">
{todoStore.filteredTodos.map(todo => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => todoStore.toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => todoStore.removeTodo(todo.id)}>
Delete
</button>
</li>
))}
</ul>
</div>
);
});

export default TodoPage;

MobX Best Practices in Next.js

  1. Keep stores modular: Create separate stores for different domains of your application.

  2. Use computed values: Derive values from state rather than storing computed results.

  3. Minimize observer components: Only wrap components that actually need reactivity with observer().

  4. Handle SSR properly: Use the pattern shown above to ensure consistent hydration between server and client.

  5. Use store injection: Pass stores through context rather than importing them directly in components.

  6. Avoid circular dependencies: Structure your stores to avoid circular dependencies between them.

  7. Prefer local state when appropriate: Not everything needs to be in global state; use React's useState for component-specific state.

Summary

MobX provides an intuitive and efficient state management solution for Next.js applications. Its reactive approach simplifies tracking and updating state across your application without the boilerplate often required by other state management libraries.

In this guide, we covered:

  • Setting up MobX in a Next.js application
  • Creating and using observable state, actions, and computed values
  • Handling server-side rendering with MobX
  • Building practical examples with counter and todo applications
  • Best practices for MobX in Next.js projects

MobX's simplicity and flexibility make it an excellent choice for many Next.js applications, especially when you want state management with minimal boilerplate and maximum developer experience.

Additional Resources

Exercise Ideas

  1. Add a "mark all as complete" feature to the todo application
  2. Create a new store for a shopping cart application with products and cart items
  3. Implement form validation using MobX observables
  4. Create a theme switcher (dark/light mode) using MobX
  5. Add persistence to the todo app using local storage and hydrate the store on page load


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)