Advanced State Management in React with Zustand
Published on October 4, 2025
Why Modern State Management Matters
In the world of React development, managing state is a fundamental challenge. While React's built-in useState and useReducer hooks are powerful for component-level state, applications often require a more robust solution for sharing state across a complex component tree. This is where state management libraries come in.
For years, Redux was the undisputed king, but its boilerplate and complexity often felt like overkill for many projects. Today, we have a new generation of libraries that offer powerful features with a much simpler API. Among them, Zustand has emerged as a fan favorite for its minimalism, flexibility, and "unopinionated" nature.
Zustand is a small, fast and scalable bearbones state-management solution using simplified flux principles. It has a comfy API based on hooks and isn't boilerplatey or opinionated.
This post will dive deep into advanced patterns and best practices for using Zustand in a professional React or Next.js application.
The Core Concept: A Simple Store
At its heart, Zustand is incredibly simple. You create a "store," which is essentially a hook that you can call from any component to get access to state and actions.
Let's start with a basic store for managing a user's session.
// src/stores/userSessionStore.ts
import { create } from 'zustand';
interface SessionState {
token: string | null;
userId: string | null;
login: (token: string, userId: string) => void;
logout: () => void;
}
export const useSessionStore = create<SessionState>((set) => ({
token: null,
userId: null,
login: (token, userId) => set({ token, userId }),
logout: () => set({ token: null, userId: null }),
}));
Using this in a component is trivial:
// src/components/LoginButton.tsx
import { useSessionStore } from '../stores/userSessionStore';
function LoginButton() {
const { login, logout, token } = useSessionStore();
if (token) {
return <button onClick={logout}>Logout</button>;
}
return <button onClick={() => login("jwt-token-123", "user-456")}>Login</button>;
}
Advanced Pattern 1: Splitting Actions and State
As stores grow, it can be beneficial to separate state from the actions that modify it. This improves readability and organization. Zustand doesn't enforce a specific structure, giving you the freedom to do this.
Let's refactor our session store:
// src/stores/userSessionStore.ts (Refactored)
import { create, StateCreator } from 'zustand';
// 1. Define the state shape
export interface SessionState {
token: string | null;
userId: string | null;
}
// 2. Define the actions
export interface SessionActions {
login: (token: string, userId: string) => void;
logout: () => void;
}
// 3. Create the store slice
const createSessionSlice: StateCreator<SessionState & SessionActions> = (set) => ({
token: null,
userId: null,
login: (token, userId) => set({ token, userId }),
logout: () => set({ token: null, userId: null }),
});
// 4. Export the final store hook
export const useSessionStore = create(createSessionSlice);
This pattern becomes invaluable when you start composing multiple stores together.
Advanced Pattern 2: Middleware and Persistence
One of Zustand's most powerful features is its middleware support. The most common use case is persisting state to localStorage so it survives page refreshes.
Here's how you can easily add persistence to our session store:
// ... imports
import { persist, createJSONStorage } from 'zustand/middleware';
// ... SessionState and SessionActions interfaces
// Wrap your store creator with the `persist` middleware
export const useSessionStore = create(
persist<SessionState & SessionActions>(
(set) => ({
token: null,
userId: null,
login: (token, userId) => set({ token, userId }),
logout: () => set({ token: null, userId: null }),
}),
{
name: 'user-session-storage', // Name for the localStorage key
storage: createJSONStorage(() => sessionStorage), // (optional) defaults to localStorage
}
)
);
With just a few lines of code, our user's session will now be automatically saved and rehydrated!
Advanced Pattern 3: Computed State with Getters
Sometimes, you need to derive state from other pieces of state. While you could do this inside your component, defining it once in the store is much more efficient.
Let's add a computed property isLoggedIn to our session store.
// ... imports
interface SessionState {
token: string | null;
userId: string | null;
isLoggedIn: boolean; // Add the computed property
}
// ... SessionActions
export const useSessionStore = create<SessionState & SessionActions>((set, get) => ({
token: null,
userId: null,
// The getter uses the `get()` function to access current state
get isLoggedIn() {
return get().token !== null;
},
login: (token, userId) => set({ token, userId }),
logout: () => set({ token: null, userId: null }),
}));
This is a clean and declarative way to handle derived state without cluttering your components.
For further reading, check out the official Zustand documentation on GitHub. Happy coding!