skip to content

In Redux, what does the preloadedState argument to createStore do, and how does it interact with each reducer's default state?

level: middleimportance: should knowfreq 45%

answer

  1. second argument to createStore
  2. default parameters apply only to undefined
  3. missing slices fall back to defaults
  4. unknown keys warned about and dropped

basics

~20 s

preloadedState is the store's starting state, used to hydrate from the server or restore a saved session; it wins over a reducer's default, and with combineReducers any slice it omits falls back to that slice reducer's own default.

solid answer

~40 s

`createStore(reducer, preloadedState, enhancer)` passes `preloadedState` to the reducer as the state for the initial `@@redux/INIT` action. A reducer's `state = default` parameter only applies when the state is `undefined`, so a supplied value always wins. With a root reducer from `combineReducers`, the preloaded object must match its keys: slices that are present receive their preloaded value, slices that are missing receive `undefined` and fall back to their reducer's default, and in development Redux warns about unexpected keys and drops them. Every reducer still needs a default, because `combineReducers` probes each slice reducer with `undefined`, and if one returns `undefined` the combined reducer throws on its first call, which is store creation. Redux Toolkit's `configureStore` takes the same value as its `preloadedState` option.

code

ts · 12 lines
ts
import { legacy_createStore as createStore, combineReducers, type UnknownAction } from 'redux'

const user = (state: { name: string } | null = null, _action: UnknownAction) => state
const cart = (state: string[] = [], _action: UnknownAction) => state

const rootReducer = combineReducers({ user, cart })

const store = createStore(rootReducer, { user: { name: 'Ada' } })

console.log(store.getState())
// { user: { name: 'Ada' }, cart: [] }
// user came from preloadedState; cart fell back to its reducer's default

go deeper

for a junior

Know that preloadedState is createStore's second argument and that it sets the store's starting value, for example state sent from the server.

for a middle

Explain why a supplied value beats a reducer's default parameter, and how combineReducers fills missing slices from their own defaults and drops unknown keys.

for a senior

Talk about hydration safely: escaping serialized state, versioning persisted slices, and which slices are worth persisting at all.

for a principal

Weigh what should be hydrated into a client store versus refetched, considering payload size, staleness and the security cost of shipping state inside HTML.

## Two ways to get initial state A Redux store gets its first state in one of two ways: - **From the reducers.** Each reducer declares a default, usually with a default parameter: `function counter(state = 0, action)`. When the store is created it dispatches a private `@@redux/INIT` action; a reducer that receives `undefined` returns its default. - **From `preloadedState`.** The optional second argument of `createStore(reducer, preloadedState?, enhancer?)` is passed to the root reducer as the state for that first INIT call. The rule that ties them together is plain JavaScript: **a default parameter only applies when the argument is `undefined`**. So if you pass a preloaded value, the reducer's default is never used for that part of the tree. ## A single reducer With a single root reducer the result is simple: 1. `createStore(counter)` → INIT runs with `undefined` → state is `0`. 2. `createStore(counter, 42)` → INIT runs with `42` → state is `42`. Preloaded state wins, completely. ## A combined root reducer With `combineReducers({ user, cart })` the behaviour is more useful: - The preloaded value must be a **plain object whose keys match** the reducer keys. - For each key, the combined reducer passes `preloadedState[key]` to that slice's reducer. - A slice **present** in the preloaded object starts with that value. - A slice **missing** from it receives `undefined` and falls back to its own default. - A key **with no matching reducer** triggers a development warning ("Unexpected key ... Unexpected keys will be ignored.") and is gone from the state as soon as the store is created, because the combined reducer only builds keys it has reducers for. So `createStore(root, { user: savedUser })` produces `{ user: savedUser, cart: <cart default> }`. That is exactly what you want when you persist only part of the tree. ## Why reducers still need defaults Even if you always preload, keep the defaults: - `combineReducers` **probes** every slice reducer with `undefined` state, using the INIT type and a random unknown type; if any returns `undefined`, the combined reducer throws on its first call, so store creation fails. - Slices you did not persist still need a starting value. - Tests often create a store with no preloaded state at all. `null` is a legal "no value" default; `undefined` is not. ## What it is used for | Use | Source of the preloaded value | |---|---| | Server-side rendering | State the server built for this request, serialized into the HTML | | Session restore | A slice or two read back from storage on start-up | | Tests | A known state to start a scenario from | | Embedding | A host page handing initial data to a widget | ## Worked outcomes Assume `user` defaults to `null`, `cart` defaults to `[]`, and the root reducer is `combineReducers({ user, cart })`: | Call | Resulting state | Why | |---|---|---| | `createStore(root)` | `{ user: null, cart: [] }` | Both slices receive `undefined` and return their defaults | | `createStore(root, { user: ada })` | `{ user: ada, cart: [] }` | `user` is preloaded; `cart` falls back | | `createStore(root, { user: ada, cart: ['pen'] })` | `{ user: ada, cart: ['pen'] }` | Both slices preloaded | | `createStore(root, { user: ada, theme: 'dark' })` | `{ user: ada, cart: [] }` plus a development warning | `theme` has no reducer, so it is dropped | The same logic nests: if `cart` were itself built with `combineReducers`, a partial `cart` object would be filled in key by key from the inner reducers' defaults. ## Traps - **Stale shapes.** Restoring an old persisted shape into a newer reducer does not throw on its own; a slice may start with fields your reducer no longer expects. Version what you persist. - **Unsafe injection.** When the server writes preloaded state into a `<script>` tag, serialize it safely (Redux's server-rendering guide escapes `<` as `\u003c`) so user-controlled strings cannot close the tag. - **Argument juggling.** `createStore(reducer, enhancer)` is also legal: if the second argument is a function and there is no third, Redux treats it as the enhancer. Passing both a function as preloaded state and an enhancer throws. - **Toolkit.** With Redux Toolkit you pass the same value as `configureStore({ reducer, preloadedState })`, and the same merging rules apply because it calls `createStore` underneath.

  • Why does Redux recommend passing window.__PRELOADED_STATE__ straight into the store and then deleting the global?
    The value is only needed once, to seed the store. Passing it directly and deleting the global avoids keeping a second reference to a potentially large object, so it can be garbage collected, and it stops other scripts from reading or tampering with it later.
  • If a persisted slice is from an older app version, what happens when you preload it?
    Redux does not validate slice contents, so the reducer simply starts from the old shape and may crash or misbehave on missing fields. Store a version number with persisted data and migrate or discard mismatched slices before passing them as preloaded state.

saying these in an interview costs you the question

  • The reducer's default parameter overrides preloadedState
  • Preloading one slice wipes the other slices to undefined
  • Reducers no longer need a default once preloadedState is passed
  • Unknown keys in preloadedState are kept in the store untouched
  • preloadedState is merged in by a special action you must handle