Managing Large Collections with NgRx Entity Adapter
Stop iterating through arrays in your NgRx store. Learn how NgRx Entity Adapter normalizes state into ID-keyed maps for O(1) lookups and efficient updates.
03 Oct 2026, 22:21 UTC

When an Angular application manages lists of data—such as a directory of users or a catalog of products—storing them as simple arrays in the store creates a performance bottleneck. Updating a single item requires iterating through the entire array or mapping over it to create a new reference, which triggers unnecessary re-renders across the UI. The NgRx Entity Adapter solves this by normalizing state into a map keyed by ID, turning O(n) search operations into O(1) lookups.
The Normalization Shift
Normalization is the process of separating data into a lookup table. Instead of an array of objects, the Entity Adapter maintains two distinct properties: ids (an array of unique identifiers) and entities (a dictionary where keys are IDs and values are the objects). This structure ensures that updating one entity does not require touching the rest of the collection, significantly reducing the computational overhead during state transitions.
Implementing the Adapter Pattern
To implement this, you define an adapter for your specific model. This adapter provides a set of predefined reducer methods that handle the immutable updates for you. Below is a practical implementation for a Product entity.
import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
import { createReducer, on } from '@ngrx/store';
import { Product } from './product.model';
import * as ProductActions from './product.actions';
// Define the adapter
export const adapter: EntityAdapter<Product> = createEntityAdapter<Product>();
// Initial state includes the entity map and any additional metadata
export interface ProductState extends EntityState<Product> {
selectedProductId: string | null;
loading: boolean;
}
export const initialState: ProductState = adapter.getInitialState({
selectedProductId: null,
loading: false,
});
export const productReducer = createReducer(
initialState,
on(ProductActions.loadProductsSuccess, (state, { products }) => {
// setAll replaces the entire collection
return adapter.setAll(products, state);
}),
on(ProductActions.updateProduct, (state, { update }) => {
// updateOne takes an entity ID and a partial object
return adapter.updateOne(update, state);
}),
on(ProductActions.removeProduct, (state, { id }) => {
return adapter.removeOne(id, state);
})
);
Efficient Data Retrieval
The adapter provides built-in selector factories that allow you to retrieve data without manually parsing the map. You should use selectAll for lists and selectEntities when you need to look up a specific item by its ID in a component.
import { createFeatureSelector, createSelector } from '@ngrx/store';
export const selectProductState = createFeatureSelector<ProductState>('products');
// Returns an array of all products
export const selectAllProducts = createSelector(
selectProductState,
(state) => adapter.getSelectors().selectAll(state)
);
// Returns the dictionary for O(1) lookup
export const selectProductEntities = createSelector(
selectProductState,
(state) => adapter.getSelectors().selectEntities(state)
);
Trade-offs and Performance Limits
While normalization improves lookup speed, it introduces a layer of boilerplate. Every action must be mapped to an adapter method, and the state structure is more rigid than a plain array. Additionally, if your entity set grows to thousands of items, selectAll can become a bottleneck because it reconstructs the array from the map on every state change. In these scenarios, you must implement pagination or use selectIds to only render the visible subset of the data.
Verifying the State Structure
To verify that normalization is working, use the Redux DevTools browser extension. Inspect the state slice; you should see a distinct ids array and an entities object. If you see a nested array instead, the adapter methods were likely bypassed in the reducer. For automated verification, write a unit test that asserts adapter.getSelectors().selectEntities(state) returns an object where the keys match the values in the ids array.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.