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

Singleton Stores and SSR: The State Leakage Challenge

Hello! Welcome to the fifth module of our course on Zustand.

So far, we've covered the fundamentals of creating and using stores, advanced patterns for managing complex state, integrating middleware, and optimizing performance. Now, we'll shift our focus to a more specialized but critical topic: using Zustand in a Server-Side Rendering (SSR) environment.

This lesson addresses a common and significant challenge you'll face in this context. Our goal is to describe the challenge of state leakage when using singleton stores in an SSR context. By the end of this lesson, you will understand what state leakage is, why it happens, and the serious implications it has for your application.

Let's begin by establishing the environment where this problem occurs.

1. The Server-Side Rendering Environment

In a typical client-side rendered (CSR) React application, the entire app runs in the user's browser. Each user gets a fresh, isolated instance of your application, including its state.

Server-Side Rendering (SSR), popularized by frameworks like Next.js, works differently. The server generates the initial HTML for a page in response to a browser request. This HTML is sent to the client, allowing the user to see the page content quickly. Then, the client-side JavaScript loads and "hydrates" the static HTML, making it interactive.

The key difference lies in the server's role. A single server process handles requests from many different users concurrently. This shared nature of the server environment is the root of the problem we're about to explore.

To solidify this concept, please read the following excerpt.

A guide to state management in Next.js

This article, 'A guide to state management in Next.js', provides a concise explanation of the fundamental difference between a client-side React SPA and a server-rendered Next.js application.

Please read the section titled 'The key difference between Next.js and React SPAs'. Focus on the paragraph that begins 'With Next.js, a single instance of the app on the server is shared across all its users.'

As the article highlights, you cannot store user-specific information in a global variable on the server because different user requests will constantly override each other. This brings us to the nature of a Zustand store.

2. Zustand's Singleton Store

When you define a Zustand store at the top level of a module, like this:

import { create } from 'zustand'

export const useBearStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
}))

You are creating a singleton. A singleton is a design pattern that restricts the instantiation of a class to a single, global instance. In the context of our CSR app, this is fine—the "global" scope is the user's browser tab. But on a server, the "global" scope is the server process itself, which is shared.

To better understand the implications of singletons, especially in concurrent environments, the following video is very insightful.

The Real Reason the Singleton Pattern Exists

This video from the ArjanCodes channel discusses the Singleton pattern. While the code examples are in Python, the concepts of global shared state, invisible coupling, and thread safety are universal and directly applicable to our discussion of SSR.

Please watch the following two segments: Basic Singleton Implementation and its Problems (01:56 - 04:40). Pay attention to the issues caused by shared state. Singleton and Thread Safety Issues (09:38 - 11:45). The problem described with multi-threading is a strong analogy for what happens when multiple user requests hit a server process at the same time.

The video's discussion of thread safety is a powerful parallel. Just as multiple threads can interfere with each other by accessing a single object, multiple user requests can interfere with each other by accessing a single store instance on the server. This interference is what we call "state leakage."

3. State Leakage: The Problem in Detail

Now we can connect the two concepts: a shared SSR environment and a singleton store.

State leakage occurs when state from one user's request persists in the singleton store on the server and is then accidentally served to a different user in a subsequent request.

Imagine this sequence of events:

  1. User A logs into your application. Their request hits your Next.js server.
  2. On the server, you store User A's session data (e.g., { userId: 'user-a', name: 'Alice' }) in your global Zustand store.
  3. The server renders the page with Alice's data and sends it to her browser. The store on the server now holds Alice's data.
  4. User B requests the same page, milliseconds later. Their request hits the same server process.
  5. The server process looks at the Zustand store, which still contains { userId: 'user-a', name: 'Alice' }.
  6. The server renders the page using Alice's data and sends it to User B. User B now sees Alice's private information.

This is not a theoretical problem; it's a well-documented issue that has caused significant bugs and security vulnerabilities. The following resource is a GitHub discussion where developers, and the maintainer of Zustand, discuss this exact scenario.

Using Zustand in React Server Components

This GitHub discussion, titled 'Using Zustand in React Server Components', is a raw, real-world look at the state leakage problem. It contains bug reports, anecdotes, and technical explanations from the library's own maintainer.

As you read through the comments, focus on these key contributions: The initial post by user 'ADTC', which clearly lays out the problem. The anecdote from 'StefanBurscher' about his 8-hour debugging session where one user's login data was visible to all other users. The technical explanation from 'dai-shi' (a maintainer), who states: 'Because on the server the module is shared across session (like users).' The succinct summary from 'dannobytes': 'the problem is really that any global singleton...will persist on the server...' The security-focused example from 'alexomon018' about sharing an accessToken.

The key takeaways from this discussion are powerful:

  • The Root Cause: A store defined at the module level is created only once per server process, not once per request. This makes it a shared, mutable global object.
  • The Consequence: User-specific data (authentication state, personal information, API error messages) can leak between different users.
  • The Severity: This is not just a UI glitch. As the accessToken example shows, it's a critical security vulnerability.

The fundamental principle, as one user put it, is that "there's no such thing as state on the server." Any attempt to use a client-side state management pattern naively on the server is bound to fail because the server is stateless and shared by design.

Conclusion

In this lesson, we've dissected a critical challenge in modern web development. Let's summarize the key points:

  • Singleton Stores: Zustand's default create API produces a singleton store, which is a single, global instance.
  • SSR Environment: Server-side rendering frameworks like Next.js use a single server process to handle requests from multiple, concurrent users.
  • State Leakage: Combining a singleton store with an SSR environment leads to state leakage, where data from one user's request can persist and be incorrectly served to another user.
  • Impact: This results in data privacy breaches, security vulnerabilities, and unpredictable application behavior.

Understanding this problem is the essential first step. You now know what to avoid and why.

Preview of the Next Lesson

Now that we've thoroughly defined the problem, our next lesson will focus on the solution. We will learn how to implement a pattern that creates a transient, per-request store in a Next.js application. This ensures that every user request gets its own isolated state, completely preventing the possibility of leakage.

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

Sign up