Top 5 Ways to Misuse NGRX Selectors

In my 6 years of experience with Angular and NGRX, I’ve noticed that most developers (including me) don’t get selectors right a lot of times. And that’s a problem because selectors are probably the most important part of NGRX when it comes right down to it since it is getting the slice of state that we worked so hard to build up. There are things to know to get it right, so I’ve decided to do a series of articles on NGRX Selectors.

The first article (this one) will focus on the top 5 ways I’ve seen developers misuse NGRX selectors. This list might be different for others but for me, this is the top 5.

These are the articles I plan to write in the future (I’ll link them when they are ready):


    • A Complicated NGRX Selectors Example
      • – This is important because most documentation or examples you see on selectors is basic and doesn’t get as full featured as you’d like
    • Unit testing NGRX Selectors
        – This is important because one of the reasons we use selectors is to move code out of the component and into simple pure functions – which makes it easy to unit test

#1 Putting logic in the component and not the selector

 

This is by far the most common thing I see: the developer gets an object on state using a selector and then uses its properties to set other variables in the component.

Consider this example:

this.store.pipe(select(getAppointment))
  .subscribe(appt => {
      this.showButton = appt.status == ‘A’ || appt.status == ‘X’;
  });

Why not do this logic in the selector and return a boolean to set showButton?

  • This assumes that appt.status is only used for this purpose
  • The component doesn’t need to know about the appointment status, it just needs to know when to show the button, and the selector can tell just that.

So, the selector becomes:

export const getShouldShowButton = createSelector(getAppointment,
  (appt) => {
    return appt?.status == ‘A’ || appt?.status == ‘X’;
  }
);
…and the component looks like this:
shouldShowButton$: Observable<boolean>;

this.shouldShowButton$ = this.store.pipe(select(getShouldShowButton));
Then, the template can use shouldShowButton$ | async to determine visibility.

NOTE: By the way, it’s a good idea, when creating things that are unique to a view to create a separate selector file with the component name like so: view-appointment.component.selector.ts


  • Store that in the store, not with the component so it’s easy for other developers to find.
    • — I know, the case can be made that it would be easier to find it along with the component but I like to keep my NGRX artifacts together (not gonna die on this hill).
  • See #2 for more information on this practice.

    #2 Not creating view models for a component using selectors

    You may need to create a view model (interface or class) for a component using selectors when:

    • A component has many properties that (maybe) come from many different selectors
    • A component has a lot of selector subscribes
        — Of course there are situations where you need a lot of selector subscribes, I’m just saying it could be a red flag that a view model could be used to simplify the component.

    Right off the bat, though, you should know that this view model isn’t to be stored in state. That is a big no-no in NGRX. We should have only one source of truth for the data. This section is talking about returning an object that is assembled using selectors from state.

    Let’s see an example of how to do this.
    It’s good practice to create a class that has a constructor for ease of creation:

    export class PeopleComponentViewModel {
      name: string;
      hair_color: string;
      gender: string;
      height: string;
    
      constructor(vm?: Partial<PeopleComponentViewModel>) {
        if (vm) {
          Object.assign(this, vm);
        }
      }
    }
    
    • In this case, the PeopleComponent only needs 4 properties of a much bigger class

    — This is derived from the People object from the Star Wars API

          • name string — The name of this person.
          • birth_year string — The birth year of the person, using the in-universe standard of BBY or ABY – Before the Battle of Yavin or After the Battle of Yavin. The Battle of Yavin is a battle that occurs at the end of Star Wars episode IV: A New Hope.
          • eye_color string — The eye color of this person. Will be “unknown” if not known or “n/a” if the person does not have an eye.
          • gender string — The gender of this person. Either “Male”, “Female” or “unknown”, “n/a” if the person does not have a gender.
          • hair_color string — The hair color of this person. Will be “unknown” if not known or “n/a” if the person does not have hair.
          • height string — The height of the person in centimeters.
          • mass string — The mass of the person in kilograms.
          • skin_color string — The skin color of this person.
          • homeworld string — The URL of a planet resource, a planet that this person was born on or inhabits.
          • films array — An array of film resource URLs that this person has been in.
          • species array — An array of species resource URLs that this person belongs to.
          • starships array — An array of starship resource URLs that this person has piloted.
          • vehicles array — An array of vehicle resource URLs that this person has piloted.
          • url string — the hypermedia URL of this resource.
          • created string — the ISO 8601 date format of the time that this resource was created.
          • edited string — the ISO 8601 date format of the time that this resource was edited.
    • Also, height is a calculated property

    Here is the selector:

    import { createSelector } from '@ngrx/store';
    
    import {
      getPersonGender,
      getPersonHairColor,
      getPersonHeight,
      getPersonName,
    } from './selectors';
    import { PeopleComponentViewModel } from '../../models/people.component.view-model';
    
    /**
     * For these calculations, see this site:
     * https://www.thecalculatorsite.com/conversions/common/cm-to-feet-inches.php
     */
    export const getHeightFormatted = createSelector(getPersonHeight, (height) => {
      const heightInCm = height;
      const heightInFeetFractional = heightInCm / 30.48;
      const heightInFeet = Math.floor(heightInFeetFractional);
      const remainderInches = Math.round(frac(heightInFeetFractional) * 12);
      return `${heightInFeet}' ${remainderInches}"`;
    });
    
    /**
     * For this calculation, see this site:
     * https://gist.github.com/Nachasic/21259aae50d0c798b5c28edb3547b318
     */
    const frac = (num: number) =>
      +num.toString().replace(Math.trunc(num).toString(), '0') * Math.sign(num);
    
    export const getPeopleComponentData = createSelector(
      getPersonName,
      getPersonHairColor,
      getPersonGender,
      getHeightFormatted,
      (name, hair, gender, height) => {
        if (name) {
          return new PeopleComponentViewModel({
            name: name,
            hair_color: hair,
            gender: gender,
            height,
          });
        }
        return null;
      }
    );
    
    • I’m calculating the height from cm to feet inches in this case but you may wonder why I didn’t just create a pipe to do the same thing
        — It’s a good point but this is why I decided to do it in a selector:

            • Since the selector is memoized (see #5 below), it will only run when one of the parms changes so it will have the same performance as a pipe
            • I can keep the business logic in the selectors and do easy unit testing (I know, it’s not hard to unit test pipes but it’s easier to unit test pure functions)

    #3 Subscribing to selectors the wrong way

    Another thing I’ve seen most developers do is subscribe to selectors in the component the wrong way.

    Subscribing to selectors for use in just the template is easy enough:

      personComponentData$: Observable<PeopleComponentViewModel>;
      additionalApiData$: Observable<AdditionalApiViewModel>;
    
      ngOnInit(): void {
        this.personComponentData$ = this.store.pipe(select(getPeopleComponentData));
        this.additionalApiData$ = this.store.pipe(select(getAdditionalApiData));
      }
    
    It’s really when the developer is subscribing to a selector they need to use in the component typescript where things can go bad.

    Consider the following example where I have a Star Wars API project and I’m getting the person ID from the URL (in router state). There is also a text field where you can change the person ID and it is bound to selectedPerson. To do this right, many things need to be accounted for:

    • The URL must be updated when the person ID changes from the text field
    • The text field must be updated when navigating to the page from the URL
    • We don’t want to get the person from the API if the last person id is equal to the current (user hits back button)
    • When the user navigates away from the page, the selector shouldn’t get invoked so we don’t try to get the person with an undefined value.

    The following code is what should be done (the notes will show where I’ve seen developers miss it):

        this.subscription.add(
          this.store
            .pipe(
              select(getStarWarsContainerComponentData),
              filter((vm) => vm != null && vm.currentPersonId != null)
            )
            .subscribe((vm) => {
              this.selectedPerson = vm.currentPersonId;
    
              if (vm.shouldGetPerson) {
                this.getPerson();
              }
            })
        );
    
    • The filter takes care of when the user navigates away and the currentPersonId = undefined
        — Some developers don’t account for this

            • It could be that long running APIs are called uselessly because of it
            • Or errors are logged that shouldn’t be
    • Setting this.selectedPerson = vm.currentPersonId populates the text field
    • I have been using the “select” inside the pipe to be in line with the new rxjs syntax
        — I see many developers still doing this.store.select() – which I understand since the docs are still using it
    • The getPerson method is using the selectedPerson to call the API
    • Remember to unsubscribe in the ngOnDestroy
        — Another thing I see developers missing
    • The shouldGetPerson Boolean is calculated in the selector:
    export const getStarWarsContainerComponentData = createSelector(
      getLastPersonId,
      getPersonNumberFromRoute,
      (lastPersonId, personNumberFromRoute) => {
        return new StarWarsContainerComponentViewModel({
          currentPersonId: personNumberFromRoute,
          shouldGetPerson:
            personNumberFromRoute != null &&
            personNumberFromRoute.length > 0 &&
            personNumberFromRoute != lastPersonId,
        });
      }
    );
    
    • We store lastPersonId in state to keep track of that
      • — I thought of returning lastPersonId to the component but then we are doing logic there when it could be done in the selector
    • I’ll show the getPersonNumberFromRoute selector in the next section

    #4 Using the router instead of Router State selectors for params

    When your project is using NGRX, you might as well take advantage of router state to get your parameter or query string values. This is something I see ignored quite often.

    Let’s look at how this is achieved.

      1. Make sure @ngrx/router-store is installed
      2. Import this in the app.module:
    StoreRouterConnectingModule.forRoot()
      3. Add the routerReducer to root state:
    router: routerReducer
      4. Add this code to a file called router.selectors.ts:
    import { getSelectors } from '@ngrx/router-store';
    
    export const {
      selectCurrentRoute, // select the current route
      selectFragment, // select the current route fragment
      selectQueryParams, // select the current route query params
      selectQueryParam, // factory function to select a query param
      selectRouteParams, // select the current route params
      selectRouteParam, // factory function to select a route param
      selectRouteData, // select the current route data
      selectUrl, // select the current url
      selectTitle, // Select the title if available
    } = getSelectors();
    
          a. “getSelectors()” is a function you import from @ngrx/router-store
          b. It defines a bunch of const selectors/factories that you can include in your selectors

      5. For example, if you want to grab a parameter, use selectRouteParam:

    export const getPersonNumberFromRoute = selectRouteParam('personNumber');
    …and the route is this:
      {
        path: 'person/:personNumber',
        component: StarwarsContainerComponent,
      },
    
          a. So, when the URL is this: http://localhost:4200/person/13
          b. getPersonNumberFromRoute = 13

    #5 Not understanding memoization

    Memoization is defined as:

    • Memoization is a programming technique that accelerates performance by caching the return values of expensive function calls. A “memoized” function will immediately output a pre-computed value if it’s given inputs that it’s seen before.
    • Memoization is a specific form of caching that lends itself to scenarios where a costly function is executed repeatedly, sometimes with the same arguments. Provided that the function is pure so that it always produces the same value from a particular set of inputs, memoizing it can increase efficiency and reduce wasted CPU cycles.

    NGRX selectors are memoized functions.

      When using the createSelector and createFeatureSelector functions @ngrx/store keeps track of the latest arguments in which your selector function was invoked. Because selectors are pure functions, the last result can be returned when the arguments match without reinvoking your selector function. This can provide performance benefits, particularly with selectors that perform expensive computation. This practice is known as memoization.

    If your code is always using just the feature or root state, it might be a code smell that memoization could be used.

    Example of doing it wrong:

    export const getAppState = createFeatureSelector<IApp>(userFeatureKey);
    
    export const getCurrentPerson = createSelector(
      getAppState,
      (state) => state?.currentPerson
    );
    
    export const getPersonName = createSelector(
      getAppState,
      (state) => state?.person?.name
    );
    export const getPersonHairColor = createSelector(
      getAppState,
      (state) => state?.person?.hair_color
    );
    
    • Doing state?.person?.name or hair_color is wrong because every time the app state changes, person.name and hair_color will be recalculated
    • It should be written so that only when the person changes, we recalculate name:
    export const getPersonName = createSelector(
      getCurrentPerson,
      (person) => person?.name
    );
    export const getPersonHairColor = createSelector(
      getCurrentPerson,
      (person) => person?.hair_color
    );
    
    • Notice here I use getCurrentPerson instead of getAppState
    • Fixing issues like this can have a big impact on performance in your application

    I’ve also seen where a developer brings in all of state into a component and does his thing in there. That breaks almost every rule in this article! 😊

    Conclusion

    Understanding what you are doing with selectors is very important in the world of NGRX. As you can see from this top 5 list, it can affect performance, readability, unit testability and more.

    The sauce for the why behind the what in this article can be found in these posts:

    About Intertech

    Intertech is a Software Development Consulting Firm that provides single and multiple turnkey software development teams, available on your schedule and configured to achieve success as defined by your requirements independently or in co-development with your team. Intertech teams combine proven full-stack, DevOps, Agile-experienced lead consultants with Delivery Management, User Experience, Software Development, and QA experts in Business Process Automation (BPA), Microservices, Client- and Server-Side Web Frameworks of multiple technologies, Custom Portal and Dashboard development, Cloud Integration and Migration (Azure and AWS), and so much more. Each Intertech employee leads with the soft skills necessary to explain complex concepts to stakeholders and team members alike and makes your business more efficient, your data more valuable, and your team better. In addition, Intertech is a trusted partner of more than 4000 satisfied customers and has a 99.70% “would recommend” rating.