skip to content

A server-rendered React app exports its Redux store as a module-level singleton, and users occasionally see another user's data. Why, and how should store creation and hydration work?

level: seniorimportance: should knowfreq 35%

answer

  1. modules evaluate once per process
  2. concurrent requests share one object
  3. a store factory, called per request
  4. serialize, escape, preload on client

basics

~20 s

A module-level store is created once per server process and shared by every concurrent request, so one user's dispatches leak into another's render; create a fresh store per request, serialize its state into the page safely, and preload the client's own store from it.

solid answer

~40 s

On the server, a module is evaluated once and then cached, so `export const store = createStore(...)` is one object shared by every request the process handles. Request A dispatches `user/loaded`, request B renders before A's data is replaced, and B's HTML contains A's user. The fix is a **factory**: export `makeStore(preloadedState?)` and call it inside the request handler, so each request fills, renders and serializes its own store. The server then writes `store.getState()` into the page, escaping `<` so state text cannot close the script tag. The browser creates **its own** store with that value as `preloadedState` and hydrates. On the client a single long-lived store per page is fine, because the browser serves only one user.

code

ts · 16 lines
ts
// store.ts: export a factory, never a shared instance
import { configureStore, combineReducers } from '@reduxjs/toolkit'
import { userReducer } from './userReducer'
import { cartReducer } from './cartReducer'

const rootReducer = combineReducers({ user: userReducer, cart: cartReducer })
export type RootState = ReturnType<typeof rootReducer>

export function makeStore(preloadedState?: Partial<RootState>) {
  return configureStore({ reducer: rootReducer, preloadedState })
}

// server.ts: one store per request
export function serializeState(state: RootState): string {
  return JSON.stringify(state).replace(/</g, '\\u003c')
}

go deeper

for a junior

Recall that on a server one module instance is shared by all requests, so a store must be created per request, not exported as a shared constant.

for a middle

Walk through the hydration hand-off: getState on the server, escaped JSON in the page, and preloadedState when the client builds its own store.

for a senior

Diagnose the cross-user leak under concurrency, explain why resetting a shared store cannot fix it, and audit other module-level caches for the same flaw.

for a principal

Decide what server-rendered pages should hydrate at all, balancing payload size, data exposure in page source and staleness against refetching on the client.

## Why the singleton leaks JavaScript module systems **evaluate a module once and cache it**. In the browser that is harmless: one page, one user, one store. On a server, one process handles many requests, often **concurrently**. If `store.ts` does ```ts export const store = createStore(rootReducer) ``` then every request that imports it gets **the same object**. The failure looks like this: 1. Request A arrives for user Ada and dispatches `user/loaded` with Ada's profile. 2. While A awaits another fetch, request B arrives for user Bo and starts rendering. 3. B renders from the shared store, which still holds Ada's profile, and sends that HTML to Bo. The same leak shows up more quietly as state that accumulates across requests: a cart that keeps items from a previous visitor, or a memory footprint that grows with traffic. ## The fix: a store per request - Export a **factory**, not an instance: `makeStore(preloadedState?)` returns a new store every time it is called. - Call it **inside the request handler**, so the store's lifetime matches the request. - Load the data that request needs, dispatch it into **that** store, render with it, then read `store.getState()`. - Never import a server store from shared modules such as API clients or utilities; pass it where it is needed, or let middleware reach it. Redux's own guides state the same rule: create a fresh store for every request, and in server-component frameworks do not define the store as a global. ## Handing state to the browser The server must send the state it rendered with, or the client's first render will not match the HTML. 1. **Serialize** `store.getState()` into a `<script>` tag, conventionally as `window.__PRELOADED_STATE__`. 2. **Escape it**: `JSON.stringify(state).replace(/</g, '\\u003c')`. Without this, a string in state containing `</script>` would end the script tag early and let user data inject markup. 3. **On the client**, create the browser's own store with `makeStore(window.__PRELOADED_STATE__)`, then `delete window.__PRELOADED_STATE__` so the value can be garbage collected and nothing else reads it. 4. **Hydrate** React with that store in the provider. ## Checklist for review | Question | Healthy answer | |---|---| | Where is the server store created? | Inside the request handler, via a factory | | Can two requests reach the same store? | No | | What goes into the HTML? | Only the state this user's page needs | | How is it serialized? | JSON with `<` escaped | | How does the client start? | Its own store, preloaded from that JSON | ## Why resetting the shared store is not a fix A tempting patch is to dispatch a `reset` action at the start of each request. It fails under concurrency: request B's reset wipes request A's data **while A is still rendering**, so A now leaks B's data, or renders empty. Any design where two requests can touch one store has this race, however carefully the resets are timed. Adding a lock would serialize every request through one store and throw away the server's concurrency. A store per request removes the shared object entirely, which is the only structural fix. ## Order of operations in the handler 1. Create the store: `const store = makeStore()`. 2. Load the data this page needs for this user and dispatch it into this store. 3. Render the React tree with this store in the provider. 4. Read `store.getState()` once rendering is done. 5. Serialize it with `<` escaped and embed it in the HTML response. 6. Let the store go out of scope so it can be garbage collected with the request. ## Related traps - **Sensitive data.** Everything in the serialized state is visible in page source. Do not put tokens or other users' data in the store you serialize. - **Payload size.** Serialized state is part of every server-rendered page. Preload what the first render needs, not the whole cache. - **Other module-level caches.** Middleware instances, memoized selectors keyed on global data and hand-rolled caches can leak the same way. Anything with per-user data must be created per request. - **Toolkit.** With Redux Toolkit the factory wraps `configureStore({ reducer, preloadedState })`, and the rule is identical.

  • Is a module-level store also wrong in a client-only single-page app?
    No. A single-page app that never runs on the server has one user per page, so one long-lived store per page is the normal setup. The leak only appears when the same module instance serves many users, which is the server's situation.
  • Why must the client create its store from the server's state rather than from reducer defaults?
    The HTML was rendered from the server's state. If the client starts from defaults, its first render differs from the markup, React reports a hydration mismatch and the page flickers to the default content. Preloading the same state makes the first client render match.
  • What else besides the store can leak between requests in the same way?
    Any module-level object that holds per-user data: hand-written caches, memoized selectors whose cache is keyed on global inputs, singleton API clients that store auth headers, and middleware instances with internal state. All of them need per-request creation, or must stay free of user data.

saying these in an interview costs you the question

  • Each request gets its own copy of a module-level store automatically
  • The browser can reuse the server's store instance directly
  • JSON.stringify output is safe to inline in a script tag as is
  • Resetting the shared store at the start of each request is enough
  • A client-only single-page app also needs a store per render