Use NgRx Selectors as a Query Layer for Filtered and Paginated Lists
Storing filtered copies in NgRx state leads to stale views and duplicated data. Derive visible items with memoized selectors and props instead.
23 Jul 2025, 14:24 UTC

You have a searchable list in an NgRx feature. The reducer stores both the raw items array and a filteredItems copy. A loadBooksSuccess action replaces items, but filteredItems still holds the old query results. Two components need different filters, so you duplicate filteredItems into listA and listB and keep them in sync by hand. The store is no longer a single source of truth; it is a cache of derived values that drift.
Keep the feature state focused on raw data and request status. Treat selectors as the query layer: derive the visible slice with createSelector, and pass the filter as a prop when the filter belongs to a component.
Why filtered copies go stale
When a reducer writes a derived array into state, every subsequent action must decide whether to recompute it. A load-success action usually replaces the raw entities and forgets the filter. A search action updates the filter but may not know about new entities. The result is a filteredItems array that is correct only until the next unrelated action arrives.
Derived state also breaks time-travel debugging. Stepping backward through actions replays the raw entities correctly, but the stored filteredItems snapshot may not match the filter that was active at that point in history.
Deriving the view with memoized selectors
NgRx selectors are pure functions that project state into a view shape. Because they are memoized, they recompute only when their inputs change. That makes them a natural place for filtering and pagination logic that would otherwise live in reducers or component services.
Start with a feature selector and a base selector for the raw collection:
const selectBooksState = createFeatureSelector<BooksState>('books');
const selectBooks = createSelector(selectBooksState, s => s.books);
Then add a selector that accepts props. The projector receives the raw books plus the query parameters supplied by the caller:
const selectVisibleBooks = createSelector(
selectBooks,
(books, props: { query: string; page: number; pageSize: number }) =>
books
.filter(b => b.title.toLowerCase().includes(props.query.toLowerCase()))
.slice((props.page - 1) * props.pageSize, props.page * props.pageSize)
);
Consume it from a component by passing the props object as the second argument to store.select:
store.select(selectVisibleBooks, { query, page, pageSize })
No new action is dispatched. The component is asking a question of the current state, and the selector answers it.
Memoization and props identity
NgRx selectors memoize on input identities. A selector with props recomputes when the props object identity changes, even if the values inside are equal. If a component creates a fresh { query, page, pageSize } object on every change-detection cycle, the selector will recompute every cycle and the view may re-render unnecessarily.
Pass primitive props when possible, or memoize the props object in the component. Only introduce custom memoization after measuring.
To verify behavior in your installed version, temporarily add a log statement inside the projector. Call the selector twice with the same props object, then again with a new object that has equal values. Observe whether the projector runs again. Run this in a scratch app with the same @ngrx/store version as your project, because signal-based store APIs and feature-creation sugar are newer than the classic createFeatureSelector pattern shown here.
Trade-offs and when not to derive
Derived views are not persisted and are not time-travelable. If you need to inspect exactly what a user saw at a past point in time, a selector projection alone will not give you that; you would need to reconstruct the props that were active then.
Expensive sorting or filtering can run repeatedly when many consumers subscribe to the same selector with different props. If the computation is heavy, consider indexing raw data or moving the work server-side.
Server-side pagination is a hard boundary. If your API accepts page and pageSize and returns only that slice, do not derive the page from the full list in a selector. Store the request parameters and the returned page in feature state, then select them. Selectors should stay pure and cheap.
Component Store fits truly local UI state, but it sits outside Redux DevTools and time-travel and should not hold shared data.
An actionable next step
Audit your feature state for values that are computable from raw entities and request status. Move them into selectors first, keep filters as props, and add selector tests for empty, loading, error, and filtered cases before changing components.
As a practical check, dispatch a load-success action in a scratch app, then change the query prop. Confirm the visible list updates without any new filtered-state action. If it does, the store is back to being the source of truth and the selector is doing the query work.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.