Diagnosing Whether Your NgRx Collection Needs an Entity Adapter
Signs your NgRx collection reducer is the wrong shape, pre-adoption checks, and when to keep manual array state.
16 Feb 2026, 13:24 UTC

The problem: your collection reducer is becoming a liability
Your Angular feature loads a list of users or products, and the NgRx reducer for that list keeps growing. Every action—add, update, delete—requires manual spread syntax, find index lookups, and careful immutability. If a teammate mutates an object directly, the template may not re-render because the reference stays the same.
Useful takeaway: NgRx Entity’s createEntityAdapter is worth adopting when your collection has stable, unique IDs and the UI reads individual records by ID more often than it only iterates the full list. It replaces manual spread-operator reducers with built-in cases and gives you fast lookups through a normalized entity map. It is not a fit for items without stable IDs, deeply nested relationships, or lists where order is the only thing that matters.
Recognizable condition
Look for these signs in a feature slice that manages a collection:
- The reducer repeats
...state.items, item: { ...action.payload }for every action. - Components search the array with
items.find((i) => i.id === action.payload.id). - A selector returns the entire array just so the template can display one record.
- Direct mutation passes code review because immutability is enforced manually, not by the framework.
Cause and diagnostic table
| Symptom | Likely cause | Entity adapter fit |
|---|---|---|
| Reducer boilerplate grows with every collection action | Manual array updates with spread operators | Strong fit |
| Template needs one record by ID, but selector returns the whole array | No ID-indexed lookup structure | Strong fit |
| Items lack stable unique IDs, or IDs can duplicate | Entity keys require stable, unique identifiers | Poor fit |
| Data contains deeply nested relationships | Adapter stores flat records only | Poor fit |
| Only iteration matters, and order changes constantly | Adapter preserves insertion order unless a sort comparer is configured | Weak fit |
Ordered checks before adopting the adapter
- Confirm the installed package version. Run
npm list @ngrx/entityin the Angular project root. No special permissions are required. Expected output is a version entry. Risk: reducer-builder and selector APIs differ between major versions, so match code samples to the installed release. - Audit ID stability. Verify every item has a unique, non-null ID that does not change across updates. If IDs are generated client-side and can change, the adapter is unsafe.
- Measure the read pattern. Check whether the UI reads individual records by ID more often than it iterates the full list. If yes, the adapter helps.
- Check nesting depth. Determine whether relationships are deeply nested. The adapter normalizes one flat collection; nested data still needs separate feature slices or manual normalization.
- Prototype one slice. Build a minimal feature slice with
createEntityAdapterand unit-testaddOne,updateOne, anddeleteOneto verify state shape and selector output.
Fixes tied to findings
If the checks indicate a strong fit, replace the manual array reducer with an entity adapter slice.
Example entity state and adapter
import { createEntityAdapter, EntityState } from '@ngrx/entity';
import { createReducer, on } from '@ngrx/store';
import { User } from './user.model';
import { userActions } from './user.actions';
export interface UserState extends EntityState {
selectedId: string | null;
loading: boolean;
}
export const userAdapter = createEntityAdapter({
selectId: (user) => user.id,
sortComparer: false,
});
export const initialState: UserState = userAdapter.getInitialState({
selectedId: null,
loading: false,
});
export const userReducer = createReducer(
initialState,
on(userActions.addUser, (state, { user }) => userAdapter.addOne(user, state)),
on(userActions.updateUser, (state, { user }) => userAdapter.updateOne({ id: user.id, changes: user }, state)),
on(userActions.deleteUser, (state, { id }) => userAdapter.removeOne(id, state)),
on(userActions.loadUsers, (state, { users }) => userAdapter.setAll(users, state)),
);
export const {
selectIds: selectUserIds,
selectEntities: selectUserEntities,
selectAll: selectAllUsers,
selectTotal: selectUserTotal,
} = userAdapter.getSelectors();
EntityState is the adapter’s normalized shape: an ids array plus an entities map keyed by ID. sortComparer: false preserves insertion order; provide a comparer function if sorted order matters.
Composed selectors and template binding
export const selectUserState = createFeatureSelector('users');
export const selectUserById = (id: string) =>
createSelector(
selectUserEntities,
(entities) => entities[id]
);
In a component, expose the result with the async pipe:
this.user$ = this.store.select(selectUserById(selectedId));
In a signal-based component, convert the observable with toSignal (Angular 16+):
this.user = toSignal(this.store.select(selectUserById(id)));
toSignal creates a signal that updates when the selected entity changes, suitable for templates that use change detection with signals.
Limitations
- The adapter does not automatically normalize nested relationships. Deeply nested data still needs manual normalization or separate slices.
- Sorting and filtering remain selector-level logic. The adapter preserves insertion order unless you configure
sortComparer. - Mutating entity state directly inside reducers breaks immutability and can prevent change detection from reflecting updates.
- APIs differ between NgRx major versions. Confirm
@ngrx/entitybefore copying examples.
Practical verification
- Run
npm list @ngrx/entityand record the major version. - Build a minimal feature slice and unit-test
addOne,updateOne, anddeleteOne. - Run the app with NgRx DevTools to inspect entity mutations and confirm IDs remain stable across updates.
- Compare reducer complexity and bundle impact against a manual array implementation for the same collection.
Escalation criteria
Keep manual array state if items lack stable unique IDs, deeply nested relationships dominate, or only iteration matters and order changes constantly.
Escalate to a separate feature slice or a maintainer when adapter state grows beyond one flat collection, nested relationships require cross-entity lookups, or optimistic updates must roll back across multiple entities.
Because adopting the adapter is a code refactor, rollback is reverting the feature-slice commit; no data migration is required.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.