Exploring NgRx Selectors in Depth

In my previous pieces on NgRx, particularly NgRx: Bad Practices, a recurring solution to many common pitfalls was "use selectors". This time, we take a closer look at NgRx selectors: why they exist, what advantages they bring, and how to leverage them effectively.

Defining State in NgRx

NgRx is chosen for scalable state management that remains decoupled from the view layer. However, a solid grasp of what state means and how it is handled is essential before diving deeper.

In CQRS, the architectural pattern that inspired Flux, Redux, and NgRx, state refers to the global dataset that must persist on the client during a user’s session and be accessible across various parts of the application. This state typically forms a substantial object with deeply nested properties. Direct mutation is prohibited; instead, changes happen solely through Actions processed by a Reducer. A Reducer is a pure function that computes a new state based on the incoming action. Once an action is dispatched from any location, the reducer calculates the updated state, replaces the old one, and broadcasts the change throughout the application.

In NgRx, the state is organized into Features—top-level branches of the global state object, typically mirroring real-world application modules like "users", "companies", or "orders". This structural approach offers several advantages: feature states can be lazy-loaded, parts of the global state can be initialized independently (a built-in NgRx capability), and the overall state shape becomes more intuitive and navigable. With this foundational understanding, we can move towards a more nuanced appreciation of selectors.

The Role of Selectors

Selectors are pure, straightforward functions that take the entire state and return a specific slice, possibly with minor transformations. They function as NgRx’s built-in change detection. When the state updates, NgRx does not compare old and new states to determine which observables to notify. Instead, it pushes the new state through all top-level selectors. Thanks to memoization, these selectors compare relevant state portions and only emit when a meaningful change is detected.

On their own, selector functions do very little. The real power emerges when we invoke the select method on the Store instance. Under the hood, the Store class extends RxJS Observable, making the store itself an observable stream. It introduces methods like select and dispatch, internally relying on an ActionsSubject that emits on every dispatched action. The dispatch implementation is deceptively simple—just a single line:

dispatch<V extends Action = Action>(
    action: V &
      FunctionIsNotAllowed<
        V,
        'Functions are not allowed to be dispatched. Did you forget to call the action creator function?'
      >
  ) {
    this.actionsObserver.next(action);
  }

Despite its appearance, the method simply forwards the action to the ActionsObserver, which elsewhere triggers the reducer to compute the new state. How does this tie back to selectors? When you use select to retrieve a state slice, you are essentially getting a mapped version of the Store observable. It’s akin to this:

this.store.pipe(map(state => mySelector(state)));

but with additional perks.

Selectors receive the full state (or a feature state, as we’ll see) and output a portion of it. The input is the "original state", while the output is the "derived state". When crafting selectors and designing state, certain principles apply:

  1. Selectors must be pure functions
  2. Avoid storing derived state in the store
  3. Utilize NgRx utilities such as createFeatureSelector, createSelector, and Entity.getSelectors (detailed below)
  4. Keep selectors concise and focused on a single responsibility. If different parts of the app need different views of the same state, create separate selectors or build upon existing ones with createSelector. Selector factories are another viable approach.
  5. Prefer named selectors whenever possible

Let’s now understand why selectors are such a valuable asset.

Why Selectors Matter

Using feature selectors combined with createSelector offers several key advantages:

  1. Memoization. Selectors created via createSelector are memoized, meaning they execute only when the underlying store data changes. This can yield significant performance gains by avoiding unnecessary re-computations.
  2. Easy composition. Functional programming encourages building complex functions from smaller, pure ones. This makes the codebase more readable and maintainable.
  3. Cleanliness. Tracing the source of any piece of state becomes trivial, simplifying debugging and issue resolution.
  4. Consistency. Standardizing how state is accessed promotes uniformity and reduces ambiguity.

Crafting Effective Selectors

Now that we’re committed to using selectors, let’s explore how to write them well, starting with boilerplate reduction. Consider this repetitive pattern:

const ordersFeature = createFeatureSelector(Features.Orders); 
// always keep an enum of Features
const allOrders = createSelector(ordersFeature, orders => orders.list);
const ordersLoading = createSelector(ordersFeature, orders => orders.loading.list);
const selectedOrders = createSelector(ordersFeature, orders => orders.selectedOrders);

// and so on, we can have multiple selectors related to this particular Feature State

Notice how often we repeat createSelector(ordersFeature, orders =>. We can encapsulate this into a small factory function to eliminate redundancy:

const ordersFeature = createFeatureSelector(Features.Orders); 
const selector = (selectorFn: <T>(state: FeatureState) => T) => createSelector(ordersFeature, selectorFn);

const allOrders = selector(orders => orders.list);
const ordersLoading = selector(orders => orders.loading.list);
const selectedOrders = selector(orders => orders.selectedOrders);

This is an improvement, as we clearly operate within the Orders Feature State without repetition.

Simplifying Logic with Selectors

Selectors can also streamline complex logic. Suppose we can select orders in the UI, and another UI section must show the owners of those orders (each Order has an owner property). One could add an owners property to the state, updating it each time an order is selected via a dispatched action.

However, this approach is flawed: derived state should never reside in the store. Note that owners is entirely dependent on selectedOrders. Since we already have a selector for selectedOrders, we can build a new one that returns the owners:

const owners = createSelector(selectedOrders, orders => orders.map(order => order.owner));

This creates a new selector that derives complex state from a simpler selector. The original state remains untouched—no new actions or reducer changes are necessary.

Another critical use of createSelector is combining multiple selectors, known as composition. Derived state often depends on more than one source of original state. For instance, consider having a list of Authors and a list of Books, where each book has an author property. We want to show authors alongside their book counts. The Author entity doesn’t contain a books array to avoid duplication. We have selectors for both lists. How do we derive the required view? By combining the two selectors:

const books = createSelector(bookState, state => state.books);
const authors = createSelector(authorsState, state => state.authors);

const authorsFinal = createSelector(authors, books, (authors, books) => {
  return authors.map(author => ({
    ...author,
    numberOfBooks: books.filter(book => book.author.id === author.id).length,
  });
});

In this example, we’ve created a new selector that calculates numberOfBooks by filtering the books array for each author.

Note: Some might be inclined to use combineLatest in the component to merge both streams and handle the mapping there. That is a bad practice. Prefer composing selectors with createSelector for cleaner code and memoization benefits.

Final Thoughts

NgRx is a robust library with features designed to simplify state management in large Angular applications. I’ve found that NgRx rarely includes unnecessary or obscure utilities. Harnessing its full potential requires knowing and correctly applying all the tools at your disposal. Among these, selectors stand out as perhaps the most overlooked yet powerful tool, especially for newcomers. With a deeper understanding of selectors, we can write more effective NgRx code and improve our overall development experience.