reduxjs/redux · error · Error

Expected the nextReducer to be a function. Instead, received

Error message

Expected the nextReducer to be a function. Instead, received: '${kindOf(nextReducer)}'

What it means

replaceReducer(nextReducer) swaps the active reducer, used for hot-reloading and code-splitting. The new value must be a function so dispatch can call it as (state, action) => state. Passing anything else (undefined, object, partial combineReducers result) would break the dispatch loop, so Redux type-checks up front.

Source

Thrown at src/createStore.ts:322

    const listeners = (currentListeners = nextListeners)
    listeners.forEach(listener => {
      listener()
    })
    return action
  }

  /**
   * Replaces the reducer currently used by the store to calculate the state.
   *
   * You might need this if your app implements code splitting and you want to
   * load some of the reducers dynamically. You might also need this if you
   * implement a hot reloading mechanism for Redux.
   *
   * @param nextReducer The reducer for the store to use instead.
   */
  function replaceReducer(nextReducer: Reducer<S, A>): void {
    if (typeof nextReducer !== 'function') {
      throw new Error(
        `Expected the nextReducer to be a function. Instead, received: '${kindOf(
          nextReducer
        )}'`
      )
    }

    currentReducer = nextReducer as unknown as Reducer<S, A, PreloadedState>

    // This action has a similar effect to ActionTypes.INIT.
    // Any reducers that existed in both the new and old rootReducer
    // will receive the previous state. This effectively populates
    // the new state tree with any relevant data from the old one.
    dispatch({ type: ActionTypes.REPLACE } as A)
  }

  /**
   * Interoperability point for observable/reactive libraries.
   * @returns A minimal observable of state changes.

View on GitHub (pinned to 3084fc33bb)

Solutions

  1. Log `typeof nextReducer` before replaceReducer and confirm it is 'function'.
  2. When re-splitting, rebuild the combined reducer: `store.replaceReducer(combineReducers({ ...newSlices }))`.
  3. In HMR, account for default-vs-namespace exports: `require('./reducers').default` for default, `.rootReducer` for named.
  4. Resolve circular imports before hot-reloading.

Example fix

// before
if (module.hot) {
  module.hot.accept('./reducers', () => {
    store.replaceReducer(require('./reducers'))   // module object, not the reducer fn
  })
}

// after
if (module.hot) {
  module.hot.accept('./reducers', () => {
    const nextRootReducer = require('./reducers').default
    store.replaceReducer(nextRootReducer)
  })
}
Defensive patterns

Strategy: type-guard

Validate before calling

if (typeof nextReducer !== 'function') {
  throw new TypeError(`nextReducer must be a function, got ${typeof nextReducer}`)
}
store.replaceReducer(nextReducer)

Type guard

const isReducer = (x): x is (s:any, a:any)=>any => typeof x === 'function'

Prevention

When it happens

Trigger: store.replaceReducer(undefined) — a new reducer import resolved to undefined; replaceReducer called with a slice map instead of combineReducers(map); replaceReducer(combineReducers) passing the function reference instead of calling it; circular import making the new reducer undefined at hot-reload time.

Common situations: Webpack HMR `module.hot.accept('./reducers', () => store.replaceReducer(require('./reducers').default))` where the default export is missing; code-splitting that injects a slice object instead of a recombined root reducer; refactor that renamed the reducer export.

Related errors


AI-assisted analysis of reduxjs/redux@3084fc33bb (2026-08-12). Data as JSON: /api/errors/ceb87a6a89e880a4. Report an issue: GitHub.