# How to Use Zustand with Next.js and React Server Components: A Complete Guide

> Master Zustand with Next.js React Server Components. Learn to create per-request stores and avoid hydration issues for seamless data management.

- Repository: [Poimandres/zustand](https://github.com/pmndrs/zustand)
- Tags: how-to-guide
- Published: 2026-03-06

---

**To use Zustand with Next.js and React Server Components, you must create a per-request store factory using `createStore` from `zustand/vanilla`, wrap it in a React Context provider marked with `'use client'`, and consume it only within client components to avoid hydration mismatches and RSC constraints.**

Zustand is a lightweight state management library for React, but using it with Next.js requires careful architecture to handle server-side rendering (SSR) and React Server Components (RSC). In the `pmndrs/zustand` repository, the recommended pattern involves isolating store instances per request and strictly separating client-side state from server components.

## Why Zustand Requires Special Handling in Next.js

Next.js renders pages on the server first and then hydrates them on the client. Because a Zustand store is just a plain JavaScript object, the default export is a *module-level* (global) singleton. In a server-rendered environment, this causes three distinct problems that the official guides in [`docs/learn/guides/nextjs.md`](https://github.com/pmndrs/zustand/blob/main/docs/learn/guides/nextjs.md) and [`docs/learn/guides/ssr-and-hydration.md`](https://github.com/pmndrs/zustand/blob/main/docs/learn/guides/ssr-and-hydration.md) address.

### The Per-Request Store Problem

A single server process may handle many concurrent requests. A global store would be shared across those requests, leaking state between users and causing race conditions. The solution is to create a **store factory** that returns a fresh vanilla store for each request, and inject it via React context.

### SSR Hydration Mismatches

The HTML generated on the server must match the client-side initial render, otherwise React throws hydration errors. You must initialize the store on the server, then reuse the same instance on the client by mounting the provider only on the client side.

### React Server Component Limitations

React Server Components (RSCs) run **only on the server** and cannot call hooks or read from a mutable store. According to [`docs/learn/guides/nextjs.md`](https://github.com/pmndrs/zustand/blob/main/docs/learn/guides/nextjs.md), you must keep Zustand usage strictly inside **client components** (`'use client'`), and expose the store through a provider that client components can consume.

## Creating a Per-Request Store Factory

The core implementation lives in [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts). Instead of using the default `create` export, you use `createStore` to build a factory function that generates isolated instances.

```typescript
// src/stores/counter-store.ts
import { createStore } from 'zustand/vanilla'

export type CounterState = { count: number }
export type CounterActions = {
  increment: () => void
  decrement: () => void
}
export type CounterStore = CounterState & CounterActions

export const defaultInitState: CounterState = { count: 0 }

export const createCounterStore = (
  initState: CounterState = defaultInitState,
) =>
  createStore<CounterStore>((set) => ({
    ...initState,
    increment: () => set((s) => ({ count: s.count + 1 })),
    decrement: () => set((s) => ({ count: s.count - 1 })),
  }))

```

This factory pattern ensures every request receives a unique store instance, preventing state leakage between users.

## Providing the Store via React Context

The `useStore` hook in [`src/react.ts`](https://github.com/pmndrs/zustand/blob/main/src/react.ts) bridges the vanilla store to React. You must wrap this in a Context provider to ensure the store instance is preserved across the component tree.

```typescript
// src/providers/counter-store-provider.tsx
'use client'

import { createContext, useContext, useState, ReactNode } from 'react'
import { useStore } from 'zustand/react'
import { createCounterStore, CounterStore } from '@/stores/counter-store'

export const CounterStoreContext = createContext<ReturnType<typeof createCounterStore> | undefined>(undefined)

export const CounterStoreProvider = ({ children }: { children: ReactNode }) => {
  const [store] = useState(() => createCounterStore())
  return (
    <CounterStoreContext.Provider value={store}>
      {children}
    </CounterStoreContext.Provider>
  )
}

export const useCounterStore = <T,>(selector: (s: CounterStore) => T) => {
  const ctx = useContext(CounterStoreContext)
  if (!ctx) throw new Error('useCounterStore must be used within CounterStoreProvider')
  return useStore(ctx, selector)
}

```

The `'use client'` directive is mandatory here because Zustand relies on React hooks and mutable state that cannot execute in React Server Components.

## Implementing Zustand in Next.js Router Patterns

The provider placement depends on whether you use the Pages Router or the App Router.

### Pages Router Implementation

In the Pages Router, wrap your application in [`_app.tsx`](https://github.com/pmndrs/zustand/blob/main/_app.tsx):

```typescript
// src/pages/_app.tsx
import type { AppProps } from 'next/app'
import { CounterStoreProvider } from '@/providers/counter-store-provider'

export default function App({ Component, pageProps }: AppProps) {
  return (
    <CounterStoreProvider>
      <Component {...pageProps} />
    </CounterStoreProvider>
  )
}

```

```typescript
// src/pages/index.tsx
import { useCounterStore } from '@/providers/counter-store-provider'

export default function Home() {
  const { count, increment, decrement } = useCounterStore((s) => s)
  return (
    <div>
      Count: {count}
      <button onClick={increment}>+</button>
      <button onClick={decrement}>‑</button>
    </div>
  )
}

```

### App Router Implementation

In the App Router, the layout must also be a client component to host the provider, or you must create a client boundary:

```typescript
// src/app/layout.tsx
import '@/globals.css'
import { CounterStoreProvider } from '@/providers/counter-store-provider'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <CounterStoreProvider>{children}</CounterStoreProvider>
      </body>
    </RootLayout>
  )
}

```

```typescript
// src/app/page.tsx
'use client'
import { useCounterStore } from '@/providers/counter-store-provider'

export default function Page() {
  const { count, increment, decrement } = useCounterStore((s) => s)
  return (
    <div>
      Count: {count}
      <button onClick={increment}>+</button>
      <button onClick={decrement}>‑</button>
    </div>
  )
}

```

The `'use client'` directive in [`page.tsx`](https://github.com/pmndrs/zustand/blob/main/page.tsx) ensures that Zustand hooks execute only in the browser, preventing RSC violations.

## Summary

- **Use `createStore` from `zustand/vanilla`** ([`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts)) to build a factory function that generates isolated store instances per request.
- **Wrap the store in a React Context provider** using `useStore` from `zustand/react` ([`src/react.ts`](https://github.com/pmndrs/zustand/blob/main/src/react.ts)) to ensure the instance persists across the component tree.
- **Always add `'use client'`** to any file that creates or consumes the store to respect React Server Component constraints.
- **Place the provider in [`_app.tsx`](https://github.com/pmndrs/zustand/blob/main/_app.tsx)** for the Pages Router or [`layout.tsx`](https://github.com/pmndrs/zustand/blob/main/layout.tsx) for the App Router to ensure the store is available throughout your application.

## Frequently Asked Questions

### Can I use Zustand directly in React Server Components?

No. According to the official Next.js guide in [`docs/learn/guides/nextjs.md`](https://github.com/pmndrs/zustand/blob/main/docs/learn/guides/nextjs.md), React Server Components cannot read from or write to a Zustand store because RSCs run only on the server and cannot access mutable client-side state or React hooks. You must mark any component that uses Zustand with the `'use client'` directive.

### Why does my Zustand state leak between users in Next.js?

This occurs because the default `create` export produces a module-level singleton. In a server environment, this singleton persists across concurrent requests, causing state to be shared between different users. The fix is to use the `createStore` factory pattern from [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts) and instantiate a fresh store per request via React Context, as documented in [`docs/learn/guides/nextjs.md`](https://github.com/pmndrs/zustand/blob/main/docs/learn/guides/nextjs.md).

### What is the difference between `create` and `createStore` in Zustand?

`create` (from `zustand`) is a convenience wrapper that immediately creates a singleton store and returns a hook bound to that instance, suitable for simple client-only apps. `createStore` (from `zustand/vanilla`) is the underlying factory function that returns a raw store object without React bindings, defined in [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts). You should use `createStore` when you need to instantiate stores dynamically per request or when integrating with Next.js SSR.

### How do I prevent hydration mismatches with Zustand?

Hydration mismatches occur when the server-rendered HTML differs from the client's initial render. To prevent this, initialize your store state consistently on both server and client, and ensure the provider wrapping your app only initializes the store once using `useState(() => createCounterStore())`. According to [`docs/learn/guides/ssr-and-hydration.md`](https://github.com/pmndrs/zustand/blob/main/docs/learn/guides/ssr-and-hydration.md), you should also avoid accessing browser-only APIs (like `window` or `localStorage`) during the initial server render.