Simplify CRUD in NgRx with the Entity Adapter: A Practical Guide
Tired of repetitive reducer logic for CRUD operations in NgRx? This post shows how NgRx Entity Adapter streamlines state management, provides ready‑made methods and selectors, and improves performance. Includes a step‑by‑step example, trade‑off discussion, and actionable next steps.
02 Jun 2026, 17:17 UTC

The Boilerplate Problem
In a typical NgRx setup, each CRUD operation requires a separate case in the reducer. For a single entity type, you might have something like:
export const reducer = createReducer(
initialState,
on(addItem, (state, { item }) => ({
...state,
items: [...state.items, item],
})),
on(updateItem, (state, { id, changes }) => ({
...state,
items: state.items.map(i => i.id === id ? { ...i, ...changes } : i),
})),
on(removeItem, (state, { id }) => ({
...state,
items: state.items.filter(i => i.id !== id),
}))
);
As the number of entity types grows, the reducer file becomes cluttered. Each new entity demands a fresh set of boilerplate lines, and keeping the logic consistent across entities is error‑prone.
Enter NgRx Entity Adapter
The @ngrx/entity package provides an EntityAdapter that handles the heavy lifting:
addOne,addMany,updateOne,removeOne, etc.- Automatic state normalization: entities stored in a dictionary keyed by
idplus anidsarray. - Pre‑built selectors:
selectAll,selectEntities,selectIds,selectTotal.
With the adapter, the reducer collapses to a few lines, and components can query the store with a single selector.
Hands‑On Example
1. Install the package
npm install @ngrx/entity
Run this in the project root. No special permissions are required.
2. Define the state shape and create the adapter
import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
import { createReducer, on } from '@ngrx/store';
import { addBook, updateBook, deleteBook } from './book.actions';
export interface Book {
id: string;
title: string;
author: string;
}
export interface BookState extends EntityState<Book> {
// additional fields can go here
}
export const adapter: EntityAdapter<Book> = createEntityAdapter<Book>();
export const initialState: BookState = adapter.getInitialState({
// any custom state properties
});
3. Write the reducer using adapter methods
export const bookReducer = createReducer(
initialState,
on(addBook, (state, { book }) => adapter.addOne(book, state)),
on(updateBook, (state, { id, changes }) => adapter.updateOne({ id, changes }, state)),
on(deleteBook, (state, { id }) => adapter.removeOne(id, state))
);
4. Generate selectors
export const { selectAll, selectEntities, selectIds, selectTotal } = adapter.getSelectors();
In a component you can now do:
books$ = this.store.select(selectAll);
5. Verify the state shape
- Open Redux DevTools (Chrome extension). You should see
entitiesas a dictionary andidsas an array. - Dispatch actions via the console or a test suite and assert that
selectAll()returns the expected array.
When to Use It (and When Not)
Benefits:
- Reduces boilerplate for CRUD operations.
- Ensures immutable, normalized state.
- Provides memoized selectors that avoid unnecessary re‑renders.
- Improves lookup performance (O(1) for
selectEntities).
Potential drawbacks:
- If you only manage a handful of simple objects, the adapter adds extra code that might feel overkill.
- Refactoring an existing non‑normalized state to use the adapter can be non‑trivial.
- Overusing the generated selectors without proper memoization can lead to performance hits in large lists.
Next Steps
- Integrate the adapter into your existing feature modules. Start with the most frequently updated entity.
- Write unit tests that dispatch each CRUD action and verify the selectors. Use
TestBedandprovideMockStorefor isolated tests. - Leverage the adapter in your NgRx Effects to handle async CRUD calls. The adapter’s methods remain pure, so you can dispatch the same actions from effects.
- Monitor performance with Redux DevTools. Compare the time to retrieve an item before and after normalizing.
- When the adapter’s benefits outweigh the initial refactor cost, extend its use to other entity types.
By adopting NgRx Entity Adapter, you’ll keep your store lean, testable, and scalable—especially important as your application grows.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.