Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Transient Stores in Next.js: Preventing State Leakage

Hello! Welcome back to our module on using Zustand with Server-Side Rendering.

In our previous lesson, we explored the critical problem of state leakage. We established that using Zustand's default singleton store in a shared SSR environment like Next.js is dangerous, as it can lead to one user's data being accidentally served to another.

Today, we'll build on that understanding by implementing the solution. This lesson's goal is to implement a pattern for creating transient, per-request stores in a Next.js app to prevent state leakage. You will learn the standard, recommended approach to safely manage state on the server, ensuring every user request is completely isolated.

1. Recapping the Core Problem

As a quick reminder, when you define a store like this in a file:
export const useStore = create(...)
...you create a single, global instance of that store. On a server handling multiple user requests, this shared instance is the source of state leakage.

The solution is to stop creating one global store and instead create a new, fresh store for every single request.

2. The Per-Request Store Pattern

The official and safest pattern for using Zustand with Next.js involves three key parts that work together:

  1. A Store Factory Function: Instead of exporting a store hook, we export a function that creates and returns a new store instance.
  2. A React Context Provider: We create a provider component that calls the factory function to create a store and makes that specific instance available to its children via React Context.
  3. A Custom Hook: We create a custom hook that components can use to access the store instance from the context.

This ensures that for each server request, Next.js renders a new Provider, which creates a new store, effectively isolating each user's state. Let's build this step-by-step.

The official Zustand documentation provides an excellent guide for this. We will use it as our primary reference.

Setup with Next.js - Zustand

The official Zustand documentation for Next.js outlines the challenges and provides the recommended solution. We'll be implementing the pattern it describes.

Please start by reading the introductory sections. You can find them by looking for the bullet points under the main title and the section titled 'No global stores'. This will reinforce the 'why' behind the pattern we're about to build.

3. Step 1: Create a Store Factory

Instead of using create from zustand to make a hook, we'll use createStore from zustand/vanilla. This function creates a raw store object without the React hook part. Our factory function will call this and return the result.

Setup with Next.js - Zustand

Let's look at the code for the store factory. This is the first piece of our pattern.

Find the section 'Creating a store per request'. Study the code block for src/stores/counter-store.ts. Pay close attention to the createCounterStore function. Notice how it takes an optional initState and returns the result of createStore.

Let's break down the key aspects of the createCounterStore function:

  • It's a factory: It's a function you call (createCounterStore()) to get a new store, not a hook you use (useCounterStore()).
  • It uses createStore: This is from zustand/vanilla and is ideal for this pattern because it decouples the store from React, allowing us to create it and pass it around.
  • It accepts initState: This is a crucial feature. It allows us to create the store with some initial data. While we're not using it just yet, this is the mechanism we'll use in the next lesson to pass data from getServerSideProps into the store.

4. Step 2: Provide the Store with Context

Now that we have a way to create unique stores, we need a way to provide a single, consistent instance to our React component tree for the duration of a request. This is a perfect job for React Context.

We will create a provider component that does two things:

  1. Calls our createCounterStore factory to create a store instance.
  2. Places that instance in a Context Provider for its children to consume.

A critical detail here is using useRef to hold the store instance. This ensures the store is created only once when the provider first mounts, and not on subsequent re-renders of the provider itself.

We also create a custom hook, useCounterStore, which will be the public API for our components. This hook gets the store from context and then uses the standard useStore hook to select data from it.

Setup with Next.js - Zustand

Now, let's examine the provider and custom hook that make the store available to our components.

Find the section 'Providing the store' and study the code for src/providers/counter-store-provider.tsx. Analyze how CounterStoreProvider uses useRef to create and hold the store, and how the custom useCounterStore hook consumes the context and uses useStore.

This combination of a factory, a provider with useRef, and a custom context-aware hook is the complete pattern for creating transient stores.

5. Step 3: Integrate into a Next.js App

The final step is to use our CounterStoreProvider to wrap our application. This ensures that a new store is created for every server request and that the same store is used for client-side navigations within a single session.

In a Next.js App Router application, you would wrap the children in your root layout.tsx. In a Pages Router application, you would wrap the <Component {...pageProps} /> in _app.tsx.

Setup with Next.js - Zustand

Let's see how to place the provider in a Next.js application.

Find the section 'Using the store with different architectures'. Quickly review the examples for both 'Pages Router' (_app.tsx) and 'App Router' (layout.tsx) to see where the CounterStoreProvider is placed.

By placing the provider at the root of your application, you guarantee that the entire component tree for a given request has access to the same isolated store instance.

6. A Cautionary Tale: The Alluring but Flawed Singleton Approach

When searching for solutions online, you might encounter tutorials that use a simpler approach. These often appear to work for basic cases but retain the fundamental flaw of using a singleton, leaving your application vulnerable to state leakage under concurrency. It's important to recognize this anti-pattern.

NextJS + State Management = Good Idea???

This popular video by Jack Herrington demonstrates integrating various state managers with Next.js. We'll look at the Zustand section as an example of the singleton pattern that we must avoid.

Please watch the section on Zustand (26:23 - 30:30). As you watch, notice that the store is created as a global singleton (const usePokemonStore = create(...)). Then, pay close attention to how getServerSideProps directly mutates this global store using usePokemonStore.getState().setPokemon(...).

The approach in the video is a textbook example of what the previous lesson warned against. The line usePokemonStore.getState().setPokemon(...) inside getServerSideProps is mutating a shared, global object on the server. If two users request the page simultaneously, they will race to write to the same store, leading to unpredictable results and state leakage.

This is why the official documentation is explicit about not using global stores and recommends the per-request provider pattern we've just learned. The provider pattern is more verbose, but it is the only way to guarantee safety and state isolation in an SSR environment.

Conclusion

In this lesson, we have built a robust and safe pattern for using Zustand in a Next.js application.

Here are the key takeaways:

  • The Goal is Isolation: The primary objective is to create a separate, isolated store instance for each user request to prevent state leakage.
  • The Pattern: We achieve this by combining three components:
    1. A store factory (createMyStore) using createStore from zustand/vanilla.
    2. A React Context provider (MyStoreProvider) that uses the factory and useRef to instantiate the store once per request.
    3. A custom hook (useMyStore) that retrieves the store from context and subscribes to it with useStore.
  • Avoid Singletons: Simpler-looking patterns that rely on a global singleton store are fundamentally flawed for SSR and should be avoided, even if they appear to work in simple scenarios.

You are now equipped with the correct architectural pattern for server-side state management with Zustand.

Preview of the Next Lesson

Our store factory function accepts an initState argument, but we haven't used it yet. In the next lesson, we will leverage this to solve another key SSR challenge: hydrating the client-side store with server-provided initial state. We'll see how to fetch data in getServerSideProps and pass it into our per-request store, ensuring the server and client start with the exact same state.

Can't find a good explanation? Sign up and we'll make it for you

Sign up