Stop Writing CRUD Reducers by Hand with NgRx Entity Adapter
NgRx Entity Adapter replaces hand-written CRUD reducer logic for normalized collections with ids and entities, giving you type-safe add, update and remove methods and memoized selectors.
18 Jan 2026, 00:42 UTC

Maintaining a list of items in NgRx Store quickly turns into repetitive reducer code: push to an array, find by id, map to update, filter to delete. The useful takeaway is that NgRx Entity Adapter standardizes that pattern into a normalized state shape with ids and entities, and gives you type-safe CRUD methods so you stop re-implementing the same logic.
The problem with hand-rolled list state
For a flat collection like todos, products or users, a common first approach is to keep state as an array. That works until you need efficient lookups, updates by id, or stable ordering. Arrays force linear scans and make immutable updates verbose.
Normalized state separates identity from order. An ids array preserves order, and an entities map keyed by id gives O(1) access. Entity Adapter is NgRx's helper for that shape. It is available from NgRx 8+ and has remained stable since.
What the adapter actually gives you
createEntityAdapter is a factory that returns an object with generic collection methods: addOne, addMany, updateOne, updateMany, removeOne, removeMany, upsertOne, setAll, and selectors. You supply a selectId function to tell the adapter how to identify an entity, and optionally a sortComparer.
The adapter generates an initial state with ids: [] and entities: {}. Reducers stay small because the adapter methods return a new state object with the normalized structure maintained for you. The adapter integrates with NgRx DevTools, so dispatched actions show the resulting ids and entities map.
Selectors are created via adapter.getSelectors(featureSelector). Typical selectors are selectAll, selectEntities, selectIds, and selectTotal. They are memoized with createSelector.
Worked example: todos feature
Assume an entity shape:
export interface Todo {
id: string;
title: string;
completed: boolean;
}Create the adapter and state interface in the feature file. Run the command in your Angular project root where package.json exists. No elevated permissions are required; this command only reads the dependency tree.
npm list @ngrx/storeCheck the output for a version >=8.0.0 to confirm Entity Adapter availability. Risk: older versions lack the current API surface.
Adapter definition:
import { createEntityAdapter, EntityState, EntityAdapter } from '@ngrx/entity';
export const todoAdapter: EntityAdapter<Todo> = createEntityAdapter<Todo>({
selectId: (todo) => todo.id,
sortComparer: (a, b) => a.title.localeCompare(b.title)
});
export interface TodoState extends EntityState<Todo> {
loading: boolean;
}
export const initialState: TodoState = todoAdapter.getInitialState({
loading: false
});Reducer using adapter methods:
export function reducer(state = initialState, action: TodoActions) {
switch (action.type) {
case '[Todo] Add Success':
return todoAdapter.addOne(action.todo, { ...state, loading: false });
case '[Todo] Update Success':
return todoAdapter.updateOne(action.update, { ...state, loading: false });
case '[Todo] Remove Success':
return todoAdapter.removeOne(action.id, { ...state, loading: false });
case '[Todo] Load Success':
return todoAdapter.setAll(action.todos, { ...state, loading: false });
default:
return state;
}
}Note the spread of state before passing to the adapter. Adapter methods only touch ids and entities; any extra properties must be preserved by you.
Selectors:
const { selectAll, selectEntities, selectIds } = todoAdapter.getSelectors(
(state: AppState) => state.todos
);Verification step: after dispatching an add action, open NgRx DevTools and inspect the feature slice. You should see an ids array and an entities map with keys matching the entity ids. You can also generate a test feature to compare:
ng generate @ngrx/schematics:feature todoInspect the generated reducer to see adapter usage in the schematics output.
Trade-offs and limitations
Entity Adapter assumes a flat, normalized collection. Nested or relational data, such as todos with embedded comments, does not fit directly. For relations you typically use multiple adapters and join via selectors, or keep a custom reducer for the relation.
Immutability is still your responsibility. Adapter methods return new state, but if you forget to spread additional state properties or mutate an entity before passing it in, you can introduce subtle bugs. Always treat entities as immutable inputs.
Sorting is handled by sortComparer, which runs on mutations. For large lists with complex sorting, consider whether client-side sorting is acceptable or if you need server-side ordering.
When to use it
Use Entity Adapter when you manage a collection identified by a stable id and need common CRUD operations with predictable performance. It reduces boilerplate and enforces a consistent state shape across features.
Do not force it onto deeply nested domain models. In those cases, keep the adapter for the top-level entities and handle relationships explicitly.
Start by replacing one hand-rolled list reducer with an adapter, verify the DevTools shape, and compare selector usage. The reduction in reducer code is immediate, and the normalized shape makes future features like optimistic updates easier to reason about.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.