The Problem
NgRx selectors use memoization to avoid unnecessary recalculations. But when the selector's input arguments do not change reference (even if nested properties changed), the selector returns the cached (stale) result.
Symptoms
- Store state updates correctly (visible in Redux DevTools)
- Selector returns old data despite state change
- Component displays outdated information
- Selector only updates when navigating away and back
Real Error Scenario
```typescript // reducer.ts const initialState = { users: { entities: {}, loading: false }, filter: 'all' };
export const userReducer = createReducer( initialState, on(updateUserName, (state, { id, name }) => ({ ...state, users: { ...state.users, entities: { ...state.users.entities, [id]: { ...state.users.entities[id], name } // Correctly creates new reference } } })) );
// selectors.ts export const selectAllUsers = createSelector( selectUserState, state => state.users.entities // Returns entity map );
// If entities reference changes but individual entity was mutated, // the selector returns stale data ```
How to Fix It
Fix 1: Ensure Immutable Updates in Reducer
on(updateUserName, (state, { id, name }) => ({
...state,
users: {
...state.users,
entities: {
...state.users.entities,
[id]: {
...state.users.entities[id],
name // Properly creates new object at every level
}
}
}
}))Every level of nesting must be spread to create new references.
Fix 2: Use Adapter for Entity State
```typescript const adapter = createEntityAdapter<User>(); const initialState = adapter.getInitialState({ loading: false });
export const userReducer = createReducer( initialState, on(updateUserName, (state, { id, name }) => adapter.updateOne({ id, changes: { name } }, state) ) );
// Selectors work correctly with adapter export const { selectAll: selectAllUsers } = adapter.getSelectors(); ```
Fix 3: Compose Selectors Properly
```typescript export const selectUserState = (state: AppState) => state.users;
export const selectAllUsers = createSelector( selectUserState, users => Object.values(users.entities) );
export const selectFilteredUsers = createSelector( selectAllUsers, selectUserState, (users, state) => { if (state.filter === 'all') return users; return users.filter(u => u.status === state.filter); } ); ```
Each selector in the chain receives the latest value from its input selectors.
Fix 4: Use createSelectorFactory for Custom Memoization
```typescript import { createSelectorFactory, defaultMemoize } from '@ngrx/store';
const customSelectorFactory = createSelectorFactory((projectionFun) => defaultMemoize(projectionFun, (a, b) => false) // Never memoize );
export const selectFreshData = customSelectorFactory( selectUserState, state => computeExpensiveOperation(state) ); ```