A journey into NgRx Selectors

This article dives deep into NgRx selectors and will help you understand what role that play in NgRx architecture and how they help decrease the complexity of a codebase

A journey into NgRx Selectors

I have already written two articles about NgRx, and those who have read NgRx: Bad Practices might have noticed that a lot of time the answer to the question "how do I fix this bad practice" is "use selectors". Today, we are going to dive deep into NgRx selectors, understand why they are needed, what benefits they provide, and how we can harness their power to the greatest extent.

What exactly is state in NgRx?

We use NgRx because we want a scalable state management solution independent of the view layer. But first of all, we need a clear understanding of what state is and how to manage it.

In CQRS, the design pattern upon which Flux, and consequentially Redux and NgRx are built, state is the global data that needs to be persisted on the client for the duration of a user session and can be used in different parts of an application. State is usually a large enough object with lots of nested properties. This object cannot be modified directly, but only using Actions with a Reducer function, a special abstraction in the form of a pure function, which is used to calculate a new state and update this global object. Reducers are triggered through actions dispatched from anywhere in the application, which calculates the new state in a pure functional manner, replaces the old state and notifies the entire application about that change.

In NgRx, the state usually contains Features - most top level objects in the hierarchy, branches in the form of properties of the global state object, which usually represent some real life features of our app. For example, an application can contain features like "users", "companies", "orders" and so on. A benefit of such an approach to designing the shape of our state is that we can lazy load feature states and initialize parts of the global state (NgRx allows for that out-of-the-box), and it allows to easier make sense of the state structure and understand, which data belongs where. Knowing this is usually enough is to start the journey towards a better understanding of the selectors.

What do selectors actually do?

Selectors are pure, preferably simple functions that map our entire state to some part of it, essentially providing us with a slice of the state (and sometimes making minor modifications to it). Selectors are basically the NgRx change detection mechanism. When a state is updated, NgRx doesn't do any comparison between old state and the new state to figure out what observables to trigger. It simply sends a new state through all top level selectors. And since selectors use memoization, they compare parts of the state and only pass down the state if the changes are detected.

When we write selector functions, by themselves they obviously don’t do much. It is when we use the select method on the Store instance is when the magic happens and we are able to read the data. How does this work under the hood? If we take a look at the source code for the Store class, we will notice, first of all, that it extends RxJS Observable. Essentially, the Store itself is an Observable. It adds several methods to use for itself internally, and exposes methods like select and dispatch. Inside it uses a special ActionsSubject that emits every time we dispatch an action. The code for dispatch is actually just one 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);
  }

The method looks big, but the implementation just passes the action to the ActionsObserver. Somewhere else the ActionsObserver will trigger the reducer function and calculate the new state. How does that affect the selectors? Then, when we receive the state slice via select method, we get a mapped version of the Store (remember it is an Observable). So basically using select is like doing this

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

but with added benefits.

Selectors receive the entire state (or a feature state - more about it later) and return a part of it. The entire state from which the result is calculated is called "original state", and the result is called "derived state". There are some rules when creating selectors and states:

  1. Selectors must be pure functions
  2. Never keep derived state in a store
  3. Use the tools provided by NgRx, like createFeatureSelector, createSelector and Entity.getSelectors (explained in more detail below).
  4. Selectors should be short and do one thing for one result. If two parts of an app use some state in two different forms, don't try to bastardize the selector, but either create two selectors or create the next selector from the first one using createSelector. Another approach is to write selector factories.
  5. Try to always use named selectors

Now let's understand why the selectors are, in fact, a good thing.

Benefits of using selectors

Why is it good to use Feature selectors and selectors created from them with createSelector? There is a number of benefits:

  1. Memoization. First of all, selectors created by createSelector are memoized, meaning that they won't be called unnecessarily unless the data in the store has changed. This provides an important performance boost
  2. Easy composition. In functional programming, we can compose different simple, pure functions into more complex ones, thus making the code way more readable and the whole system more maintainable
  3. Cleanliness. We can always easily find from where a particular state is coming, and debug/find issues with no hassle
  4. Consistency. It is always a good idea to do certain things in a single, concise way

Writing good selectors

So now when we have decided to use selectors in this way, let's understand how to write good selectors. Let's start with reducing boilerplate. Take a look at this code:

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

Now as you can see, we repeat this particular piece of code a lot: createSelector(ordersFeature, orders =>. We can reduce this repetition by creating a small selector factory function:

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 somewhat better, as we already obviously work with the Orders Feature State, so no need to repeat it all the time.

Reducing code complexity using selectors

Now let us use selectors to reduce code complexity. Imagine this scenario: we can select orders from the UI, and another part of the UI has to display their owners (each Order has a property owner). We could create a property on our state called owners and update it each time an Order is selected (when the corresponding action is dispatched).

But this is actually a very bad practice: never keep derived state in the store. Notice that the state of the owners depends entirely on selectedOrders. We already have a selector for selectedOrders, so let's create a new selector from it, which will return the owners:

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

Now we have created a new selector, which returns a complex derived state based on another, simpler selector. Nothing in the original state is being changed, we don't need new actions or any changes in the reducer.

Another important use case for the createSelector function is combining two selectors, aka composition. As we have learned, derived state is a mapping of one form of original state into some result via a selector. Sometimes we can also map an existing selector to a new selector, as we have seen in the previous example. But more often than you would expect, some forms of derived state depend not on one, but two (or even more!) original states.

Here is an example: Imagine we have a list of Authors, and a list of Book, with each book having an author property. Now we want to display the list of the Authors, but next to their name we want to show the number of books they have. An Author does not have an array of Books to avoid data duplication. We have selectors to choose the list of Authors and Books. How can we derive the state we need? We can combine the two selectors and create a final one:

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,
  });
});

Now in this example we created a new selector, and derived the new property numberOfBooks by scanning the books array for each author to find out how many of them have been authored by that author.  

Note: some developers might be tempted to select both streams of data in the component using combineLatest and do the mapping there. This is a bad practice. Combine selectors using createSelector instead for cleaner code and memoization.  

In conclusion

NgRx is a powerful tool with lots of features to make our lives easier when dealing with large Angular applications. And one thing in particular I find with NgRx is that it has almost no redundant or obscure features or tools. To truly harness its powers, one needs to know and correctly use all the tools it provides. And the most important tool it provides that is often overlooked by newcomers is selectors. Hopefully, with this article, we will be able to write better NgRx selectors, resulting in better experiences when developing with it.