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

SSR Transient vs. CSR Singleton Stores

Hello! Welcome to the final lesson in our module on Zustand with Server-Side Rendering.

Over the past two lessons, we've carefully constructed a robust pattern for using Zustand in a Next.js application. We created a store factory to generate per-request stores, used a React Context Provider to make them available, and implemented a hydration strategy to sync server and client state. This was a necessary and detailed process.

Today, our goal is to step back and understand the bigger picture. We will contrast the transient store pattern for SSR with the singleton store pattern for Client-Side Rendering (CSR). This lesson will clarify why the complexity of the transient pattern is essential for SSR and when you can safely use Zustand's much simpler default approach.

By the end of this lesson, you will have a clear mental model for two distinct Zustand architectures and know precisely when to apply each one.

1. The Singleton Store Pattern: The CSR Default

In a typical Client-Side Rendered (CSR) application, like one built with create-react-app, the entire application runs in the user's browser. There is only one user, one session, and one application instance at a time.

In this environment, the simplest way to use Zustand is the most effective. This is the singleton pattern.

A singleton is an object that is instantiated only once per application lifecycle. In Zustand, this is achieved by defining your store as a module-level constant.

// store.js
import { create } from 'zustand'

// The 'create' function is called once when this module is first imported.
// The resulting hook, 'useBearStore', holds a reference to a single, global store instance.
export const useBearStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}))

Any component can then import and use this hook to interact with the one and only store.

// MyComponent.jsx
import { useBearStore } from './store'

function BearCounter() {
  const bears = useBearStore((state) => state.bears)
  return <h1>{bears} around here ...</h1>
}

This approach is simple, requires minimal boilerplate, and is one of Zustand's main selling points. It works perfectly in a CSR context because the "global" scope is confined to a single user's browser tab.

To see this simple API described, let's watch a brief segment of a video that introduces Zustand's core concepts.

Global State Management in NextJs 14 Using Zustand

This video from Hamed Bahram provides a good overview of Zustand basics. We'll focus on the part that explains the create function and the resulting hook, which exemplifies the singleton pattern.

Please watch from 04:40 to 07:00. Notice how a single call to create produces a hook (useStore) that can be used anywhere. The video also highlights a key feature of this pattern: you don't need a <Provider> to wrap your app, which is a direct consequence of the store being a global singleton.

2. The Danger of Singletons in SSR

The simplicity of the singleton pattern is appealing, but it becomes a critical vulnerability in a Server-Side Rendering environment.

An SSR server (like Next.js) is a multi-user environment. A single running server process handles requests from many different users, often concurrently. If you use a singleton store here, that single store instance is shared across all user requests handled by that process.

This leads to a severe issue known as state leakage.

Imagine this scenario:

  1. User A logs in. Your server-side code fetches User A's profile and stores it in the "global" Zustand store. The page is rendered and sent to User A.
  2. User B makes a request to the same server process a moment later.
  3. The server-side code for User B's request now reads from the same global store, which still contains User A's data.
  4. User B is shown a page containing User A's private information.

This is not a theoretical problem; it's a fundamental consequence of using module-level state on a server.

Using Zustand in React Server Components

This GitHub discussion provides a stark and clear explanation of this exact problem from developers who have encountered it. It's a crucial read to understand the 'why' behind the transient pattern.

Please read the initial post by user ADTC and the reply from the maintainer dai-shi under the heading 'Replies'. Focus on the example of authenticated user data leaking between sessions. This discussion solidifies the core principle: a module-level variable on a server is shared across all requests to that server process.

As the discussion highlights, the guiding principle should be: servers are stateless. Each request should be treated as an isolated, independent transaction.

3. The Transient Store Pattern: The SSR Solution

To solve the state leakage problem, we must abandon the singleton pattern for SSR. Instead, we adopt the transient store pattern, which is exactly what we built in the previous lessons.

The core idea is to create a new, isolated store instance for every single request. This store is "transient"—it lives only for the duration of that request and is then discarded.

This is achieved with two main components we've already implemented:

  1. A Store Factory: A function (createCounterStore) that, when called, returns a brand new store instance.
  2. A React Context Provider: A component (CounterStoreProvider) that calls the factory and provides the resulting unique store instance to the component tree for that specific request.

This ensures that User A's request gets its own store, and User B's request gets a completely separate one. There is no shared state, and therefore no possibility of leakage.

The official Zustand documentation for Next.js confirms this is the required approach.

Setup with Next.js - Zustand

Let's revisit the official Zustand documentation on Next.js, which explicitly advises against global stores for this very reason.

Please review the section under the heading 'We have these general recommendations...'. The first point, 'No global stores', is a direct instruction to avoid the singleton pattern in a Next.js (SSR) context.

4. Visualizing the Lifetimes

The difference between these two patterns comes down to the lifetime of the store instance. The following diagram provides a powerful visual analogy.

This diagram illustrates different service lifetimes. In our context, the "Singleton instance" corresponds to our CSR store pattern, and the "Scoped instance" corresponds to our SSR transient/per-request store pattern.

Let's map our patterns to this diagram:

  • Singleton Pattern (CSR): This is the "Singleton instance" in the diagram. The store is created once and lives for the entire Application lifetime (i.e., as long as the user has the web page open).
  • Transient Pattern (SSR): This is the "Scoped instance" in the diagram. A new store is created for each HTTP Request. It is "scoped" to that request and destroyed once the response is sent.

5. Summary: Singleton vs. Transient

Let's summarize the key differences in a table.

Feature Singleton Pattern (CSR) Transient Pattern (SSR)
Definition A single, global, module-level store instance. A new store instance created for each request.
Environment Client-Side Rendering (e.g., SPAs). Server-Side Rendering (e.g., Next.js, Remix).
Instance Lifetime The entire application session. A single server request.
Safety Safe. The "global" scope is the user's browser. Unsafe in SSR. Causes data leakage between users.
Implementation const useStore = create(...) Store factory function + React Context Provider.
Complexity Low. Minimal boilerplate. High. Requires more setup and boilerplate.

Conclusion

You have now mastered the two primary architectural patterns for using Zustand. The choice between them is not a matter of preference but a strict requirement based on your application's rendering environment.

Key Takeaways:

  • The Singleton Pattern is the simple, default way to use Zustand. It uses a single global store and is perfect for Client-Side Rendered (CSR) applications.
  • The Transient (or Per-Request) Pattern is a more complex but necessary architecture for Server-Side Rendering (SSR). It creates an isolated store for each request to prevent critical state leakage between users.
  • The fundamental reason for this difference is that a browser is a single-user environment, while a server is a multi-user, concurrent environment.

Preview of the Next Lesson

This lesson concludes our deep dive into Zustand's integration with SSR. Having covered the core API and advanced architectural patterns, we will now broaden our scope. In the next module, we'll begin exploring the Zustand ecosystem by learning how to integrate third-party libraries for tasks like analytics and event tracking.

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

Sign up