# Handle server-rendered stores

Use a store factory and a client-side provider when a React server-rendered application needs state that is isolated per request and identical during server rendering and client hydration.

## When to use this pattern

Use this pattern when a server can handle concurrent requests or when the initial state comes from request data. A module-level store is shared by requests, so one request can observe another request's state. Create the store inside the provider instead, and pass the same serializable initial state to the server-rendered tree and the browser.

React Server Components must not read from or write to the store. Keep store access in the client component that consumes the provider; a server component can provide initial data as props.

## 1. Create a store factory

Build the store with [`createStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#createstore), not as a module-level singleton. The factory accepts the state for one request and creates actions that update that store.

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

export type CounterState = {
  count: number
}

export type CounterActions = {
  decrementCount: () => void
  incrementCount: () => void
}

export type CounterStore = CounterState & CounterActions

export const defaultInitState: CounterState = {
  count: 0,
}

export const createCounterStore = (
  initState: CounterState = defaultInitState,
) =>
  createStore<CounterStore>()((set) => ({
    ...initState,
    decrementCount: () => set((state) => ({ count: state.count - 1 })),
    incrementCount: () => set((state) => ({ count: state.count + 1 })),
  }))
```

Each call to `createCounterStore()` returns a separate vanilla store, so each provider can keep state scoped to its own request or route. See [How Zustand works](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/how-zustand-works) for the returned store API; the component below uses its React binding instead of reading the store directly.

## 2. Create the provider once per render tree

Create the store in a lazy `useState` initializer. The initializer runs once for the provider instance, so a client re-render does not replace the store. [`useStore`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#usestore) subscribes a component to the store and selects the requested value. The [`StoreApi`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#storeapi) type describes the store API held by the context.

```tsx title="src/providers/counter-store-provider.tsx"
'use client'

import { createContext, useContext, useState, type ReactNode } from 'react'
import { useStore, type StoreApi } from 'zustand'
import { createStore } from 'zustand/vanilla'

type CounterState = {
  count: number
}

type CounterStore = CounterState & {
  decrementCount: () => void
  incrementCount: () => void
}

type CounterStoreApi = StoreApi<CounterStore>

const CounterStoreContext = createContext<CounterStoreApi | undefined>(
  undefined,
)

export type CounterStoreProviderProps = {
  children: ReactNode
  initialState: CounterState
}

export function CounterStoreProvider({
  children,
  initialState,
}: CounterStoreProviderProps) {
  const [store] = useState(() =>
    createStore<CounterStore>()((set) => ({
      ...initialState,
      decrementCount: () =>
        set((state: CounterStore) => ({ count: state.count - 1 })),
      incrementCount: () =>
        set((state: CounterStore) => ({ count: state.count + 1 })),
    })),
  )

  const content = children ?? <p>Count: {store.getState().count}</p>

  return (
    <CounterStoreContext.Provider value={store}>
      {content}
    </CounterStoreContext.Provider>
  )
}

export function useCounterStore<T>(
  selector: (state: CounterStore) => T,
): T {
  const store = useContext(CounterStoreContext)

  if (store === undefined) {
    throw new Error(
      'useCounterStore must be used within CounterStoreProvider',
    )
  }

  return useStore(store, selector)
}
```

The provider owns one store for its mounted subtree. Rendering a second provider creates a second store, which is useful when separate parts of an application need separate request or route scopes.

## 3. Read and update state in a client component

Mark the component that calls the custom hook as a client component. It renders the request's initial count on both server and client, then updates that count when the user clicks a button.

```tsx title="src/components/home-page.tsx"
'use client'

import { useCounterStore } from '@/providers/counter-store-provider'

type CounterStore = {
  count: number
  incrementCount: () => void
  decrementCount: () => void
}

export function HomePage() {
  const count = useCounterStore((state: CounterStore) => state.count)
  const incrementCount = useCounterStore(
    (state: CounterStore) => state.incrementCount,
  )
  const decrementCount = useCounterStore(
    (state: CounterStore) => state.decrementCount,
  )

  return (
    <main>
      <p>Count: {count}</p>
      <button type="button" onClick={incrementCount}>
        Increment Count
      </button>
      <button type="button" onClick={decrementCount}>
        Decrement Count
      </button>
    </main>
  )
}
```

The page initially shows `Count: 3` in this example. Clicking **Increment Count** changes it to `4`; clicking **Decrement Count** changes it to `2` when starting from `3`. Each selector subscribes only to the selected value.

## 4. Pass matching initial state from the server

In the Next.js App Router, place the provider in the server-rendered layout and pass serializable request data to it. The page below represents request-derived data with a fixed value; replace that value with data loaded for the current request, without reading or writing the Zustand store in the server component.

```tsx title="src/app/layout.tsx"
import type { ReactNode } from 'react'

import { CounterStoreProvider } from '@/providers/counter-store-provider'

export default function RootLayout({ children }: { children: ReactNode }) {
  const initialState = { count: 3 }

  return (
    <html lang="en">
      <body>
        <CounterStoreProvider initialState={initialState}>
          {children}
        </CounterStoreProvider>
      </body>
    </html>
  )
}
```

```tsx title="src/app/page.tsx"
import { HomePage } from '@/components/home-page'

export default function Page() {
  return <HomePage />
}
```

The server output and the client's first render both use `count: 3`, so React hydrates matching markup. After hydration, the buttons update the client-side store. If the initial state differs between those renders, React can report a hydration error.

For the Pages Router, place `CounterStoreProvider` around `Component` in `src/pages/_app.tsx`. If only one route needs the store, place the provider in that route instead; that gives the route its own store scope.

## Options that matter

This pattern has no Zustand configuration options. The values that determine its behaviour are the factory's `initState`, the provider's `initialState`, and each selector passed to `useCounterStore`.

## Pitfalls

- Do not define the store as a module-level variable. A global store can leak state between concurrent server requests.
- Do not read or write the store from a React Server Component. Pass request data into a client provider instead.
- Keep the server's initial state and the client's initial state identical. Rendering different data produces hydration errors.
- Keep the provider's store creation inside the lazy `useState` initializer. Creating it during every render replaces the instance when the provider re-renders.

For persisted stores, asynchronous storage hydrates after the initial render; see [Persist store data](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/persist-store-data) and [Control persisted hydration](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/control-persisted-hydration). For the distinction between initializer updates and the direct vanilla-store API, see [How Zustand works](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/how-zustand-works) and [Create a vanilla store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/create-a-vanilla-store).

## Related

- [How Zustand works](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/how-zustand-works)
- [Build a React store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/build-a-react-store)
- [Vanilla and scoped stores](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/vanilla-and-scoped-stores)
- [React hydration](https://react.dev/reference/react-dom/client/hydrateRoot)
