skip to content

In NgRx 22, how would you write a meta-reducer that resets every slice, including a lazily registered orders feature, when the admin logs out?

level: seniorimportance: should knowfreq 30%

answer

  1. a reducer that wraps a reducer
  2. undefined state means initial state
  3. root config, not feature config
  4. array order: first entry is outermost
  5. META_REDUCERS when it needs injection

basics

~20 s

A meta-reducer wraps the root reducer: on the logout action it calls the inner reducer with undefined state, so every slice falls back to its initial state. Registered in provideStore's metaReducers, it also covers features added later by lazy routes.

solid answer

~50 s

A **meta-reducer** is a function `(reducer: ActionReducer<any>) => ActionReducer<any>`: it returns a reducer that can inspect or replace the state and action before and after delegating. For a logout reset it checks `action.type === AuthActions.loggedOut.type` and returns `reducer(undefined, action)`; the combined reducer then hands each slice `undefined`, and each slice's `createReducer` falls back to its initial state. I register it in `provideStore(reducers, { metaReducers: [clearOnLogout] })`. Root meta-reducers wrap the whole combined reducer, and features registered later by `provideState` are added to that same map, so the lazy `orders` slice is reset too. A meta-reducer passed in a feature's own config wraps only that slice. The array composes right to left, so the first entry is outermost and sees each action first. If the meta-reducer needs a service, it goes on the `META_REDUCERS` multi token instead.

code

ts · 20 lines
ts
import { ActionReducer, MetaReducer, provideStore } from '@ngrx/store';
import { AuthActions } from './auth.actions';

export function clearOnLogout(reducer: ActionReducer<any>): ActionReducer<any> {
  return (state, action) => {
    if (action.type === AuthActions.loggedOut.type) {
      // keep UI preferences, reset everything else to initial state
      return reducer({ preferences: state?.preferences }, action);
    }
    return reducer(state, action);
  };
}

export const metaReducers: MetaReducer[] = [clearOnLogout];

// app.config.ts
export const appConfig = {
  providers: [provideStore({ auth: authReducer, preferences: prefsReducer }, { metaReducers })],
};
// the lazily registered orders slice sits in the same root map, so it resets too

go deeper

for a junior

Recall that a meta-reducer is a function that wraps a reducer, and that NgRx treats undefined state as a request for the initial state.

for a middle

Explain how metaReducers compose right to left, and why registering at the root rather than on one feature decides which slices are affected.

for a senior

Design the logout reset or hydration so it covers lazy features, keeps chosen slices, stays pure apart from one storage edge, and registers through META_REDUCERS when it needs injected services.

for a principal

Decide which cross-cutting concerns belong in meta-reducers rather than in effects or dedicated slices, and keep their number small enough that every action's path stays predictable.

## What a meta-reducer is In the NgRx global Store, every dispatched action runs through one **root reducer**, which NgRx builds by combining the reducers of all registered slices. A **meta-reducer** is a higher-order reducer: a function that receives a reducer and returns a new reducer wrapping it. ```ts type MetaReducer = (reducer: ActionReducer<any>) => ActionReducer<any>; ``` Inside the returned function you may look at the action, change the state or action passed inward, or change the state coming back out. The NgRx docs compare meta-reducers to middleware in Redux: they are hooks into the action-to-reducer pipeline. Typical uses: - **reset on logout**: discard all user data in one place; - **hydration**: merge persisted state in when the store or a feature initialises; - **logging**: print every action and state during development; - NgRx's own **runtime checks**, which are meta-reducers too. ## The logout reset, step by step The admin area has an `orders` feature, registered lazily with `provideState(ordersFeature)`, plus root slices such as `auth`. On logout, all of them must return to their initial state: 1. The meta-reducer compares `action.type` with the logout action creator's `type`. 2. On a match, it calls `reducer(undefined, action)`. 3. The combined reducer gives each slice `undefined` as its current value. 4. Every reducer built by `createReducer` treats `undefined` as "use the initial state", so each slice is recreated from its defaults. 5. The logout action still flows inward, so a slice with an `on()` for logout can react as well. To keep one slice, such as UI preferences, pass a partial object instead of `undefined`: `reducer({ preferences: state.preferences }, action)`. Keys you leave out arrive as `undefined` and reset. ## Where you register it decides what it wraps | Registration | What it wraps | Sees the lazy orders slice? | |---|---|---| | `provideStore(reducers, { metaReducers: [...] })` | the whole root reducer | yes, since features are added to the same map | | `provideState('orders', reducer, { metaReducers: [...] })` | only the orders reducer | only its own slice | | a provider on the `META_REDUCERS` token (`multi: true`) | the whole root reducer | yes | Two details matter here: - `provideState(ordersFeature)`, the one-argument form that takes a feature object, has **no config parameter**. A feature-level meta-reducer needs the `(name, reducer, config)` form. - A reset placed at feature level resets only that feature, which is a common reason a "logout reset" leaves other slices full of the previous user's data. ## Ordering and dependency injection `metaReducers: [a, b]` is **composed right to left**: `b` wraps the real reducer and `a` wraps `b`. So `a` is outermost; it sees each incoming action first and the final state last. Put a logger first if it should record what the reset produced. NgRx collects meta-reducers from the `META_REDUCERS` token **before** those in `metaReducers`, and its runtime checks are registered on that token, so they wrap yours from the outside. A meta-reducer is created outside any component, so it cannot call `inject()` while it handles actions. When it needs a service, for example a storage wrapper for hydration, provide a factory on `META_REDUCERS` with `deps` and **`multi: true`**. NgRx itself registers its runtime checks on that token as multi providers, so a provider without `multi: true` clashes with them instead of joining the list; the docs call `multi` critical for exactly this reason. ## Hydration uses the same hook A hydration meta-reducer listens for NgRx's lifecycle actions, both exported from `@ngrx/store`: - `INIT` (`@ngrx/store/init`), dispatched once when the store starts; - `UPDATE` (`@ngrx/store/update-reducers`), dispatched whenever `provideState` adds a feature, so a lazily registered `orders` slice can be hydrated on arrival. Reading storage inside a reducer pipeline is a side effect. Keep it confined to that one meta-reducer, and make the merge produce a new object rather than mutating the state it receives. ## Pitfalls - **Registering the reset at feature level** and expecting it to clear everything. - **Mutating the state** inside the meta-reducer; the immutability check freezes state in development, so the mutation throws there and hides in production. - **Forgetting `multi: true`** on `META_REDUCERS`. - **Assuming effects and component-local stores reset too**; the meta-reducer only resets global Store state.

  • With metaReducers: [logger, clearOnLogout], does the logger see the state before or after the reset?
    The array composes right to left, so `logger` is outermost. It sees the logout action first, calls inward through `clearOnLogout`, and receives the already-reset state on the way back out. If it logs after delegating, it prints the reset state; if it logs before, it prints the previous user's state. Swapping the order would make the reset wrap the logger instead.
  • How would you hydrate the orders slice from storage when its lazy route registers it?
    Use a root meta-reducer that reacts to `UPDATE` (`@ngrx/store/update-reducers`), which NgRx dispatches when `provideState` adds a feature, and whose `features` list names the new key. After delegating, merge the persisted orders data into a new object for that key. If the storage wrapper is a service, register the meta-reducer through a `META_REDUCERS` factory with `deps` and `multi: true`.

saying these in an interview costs you the question

  • A meta-reducer registered in provideState's config resets every slice in the store.
  • Lazily registered features are not wrapped by the root meta-reducers.
  • The last meta-reducer in the array runs first on each action.
  • A meta-reducer can call inject() whenever it needs a service during an action.
  • Providing META_REDUCERS without multi: true simply adds one more meta-reducer.