Optimizing Angular State with NgRx Entity Adapter
Stop manually mapping through arrays in your NgRx reducers. Learn how to use the NgRx Entity Adapter to normalize state for O(1) lookups and cleaner CRUD logic.
04 Sept 2025, 23:07 UTC

Managing collections of objects in an NgRx store often leads to repetitive, error-prone code. When updating a single item in a large array, developers typically map over the entire collection, check for a matching ID, and spread the object to maintain immutability. As the dataset grows, this manual approach creates significant boilerplate and can degrade performance due to unnecessary iterations.
The solution is to normalize your state using the NgRx Entity Adapter. Instead of a flat array, the adapter organizes data into a dictionary (entities) keyed by ID and a separate array of IDs. This structure transforms O(n) search operations into O(1) lookups, drastically reducing the logic required for CRUD operations.
The Cost of Flat Array State
In a standard array-based state, a simple update—such as toggling a checkbox in a list of 500 items—requires the reducer to iterate through every element to find the target. This not only clutters the reducer with .map() and .filter() logic but also forces Angular's change detection to work harder if the entire array reference is replaced frequently.
By using an adapter, you delegate these operations to a set of optimized, immutable helper methods. You no longer write the logic to find an item; you simply tell the adapter which ID to update.
Defining the Adapter and State
To implement this, you define a model interface and initialize the adapter. The adapter provides the initial state structure, which includes an ids array and an entities object.
import { createEntityAdapter, EntityState, EntityAdapter } from '@ngrx/entity';
export interface Product {
id: string;
name: string;
price: number;
}
// Define the state shape, extending EntityState to add custom properties
interface ProductState extends EntityState<Product> {
selectedProductId: string | null;
loading: boolean;
}
// Initialize the adapter with an optional sort comparer
export const productAdapter: EntityAdapter<Product> = createEntityAdapter<Product>({
sortComparer: (a, b) => a.name.localeCompare(b.name),
});
export const initialState: ProductState = productAdapter.getInitialState({
selectedProductId: null,
loading: false,
});
Practical Implementation: Reducers and Selectors
The adapter provides methods like addOne, updateOne, and setAll. These methods return a new state object, ensuring immutability without manual spreading.
import { createReducer, on } from '@ngrx/store';
import * as ProductActions from './product.actions';
export const productReducer = createReducer(
initialState,
// Replace all entities (e.g., after an API call)
on(ProductActions.loadProductsSuccess, (state, { products }) =>
productAdapter.setAll(products, { ...state, loading: false })
),
// Update a specific entity by ID
on(ProductActions.updateProduct, (state, { update }) =>
productAdapter.updateOne(update, state)
),
// Remove an entity
on(ProductActions.removeProduct, (state, { id }) =>
productAdapter.removeOne(id, state)
)
);
To retrieve this data, the adapter generates memoized selectors. This prevents the UI from re-rendering unless the specific slice of state it depends on actually changes.
import { createSelector } from '@ngrx/store';
const selectProductState = (state: AppState) => state.products;
// getSelectors provides selectAll and selectEntities
export const {
selectAll: selectAllProducts,
selectEntities: selectProductEntities
} = productAdapter.getSelectors(selectProductState);
// Custom selector to get a single product by ID
export const selectProductById = (id: string) =>
createSelector(selectProductEntities, (entities) => entities[id]);
Trade-offs and Constraints
The primary requirement for the Entity Adapter is stable and unique IDs. If an entity's ID changes on the backend, the adapter will treat it as a new item, leaving the old version as a stale entry in the store. This can lead to duplicate items in the UI if not handled by a clean-up process.
Additionally, for very small, static collections (e.g., a list of 3-5 categories), the overhead of normalization may be unnecessary. A simple array is more intuitive for data that never changes or is rarely updated.
Verifying the Result
To verify the implementation, run the application and open the NgRx DevTools browser extension. Dispatch an updateOne action and inspect the state tree. You should see that the ids array remains unchanged while only the specific object inside the entities map is updated. This confirms that the update is O(1) and avoids recreating the entire collection list.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.