3d cartoon hands holding a phone

Unlock full course by purchasing a membership

Lesson 3

Refactoring using NgRx Component Store

Refactoring our simple store to use NgRx Component Store

EXTENDED

Refactoring using NgRx Component Store

We’ve already refactored the state for a component into a service that is provided to that component, which we can sort of consider as being step one for integrating @ngrx/component-store. But, as you can clearly see, the technique we have introduced can still be useful without @ngrx/component-store.

Now, we are going to actually install @ngrx/component-store and refactor this service into one that makes use of the features provided by Component Store.

Install @ngrx/component-store with the following command:

npm install @ngrx/component-store

Before we start with the refactor, here is our HomeStore currently from the last lesson:

import { Injectable } from '@angular/core';
import { FormControl } from '@angular/forms';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { RedditService } from '../../shared/data-access/reddit/reddit.service';

@Injectable()
export class HomeStore {
  currentlyLoadingGifs$ = new BehaviorSubject<string[]>([]);
  loadedGifs$ = new BehaviorSubject<string[]>([]);
  settingsModalIsOpen$ = new BehaviorSubject<boolean>(false);
  subredditFormControl = new FormControl('gifs');

  // Combine the stream of gifs with the streams determining their loading status
  gifs$ = combineLatest([
    this.redditService.getGifs(this.subredditFormControl),
    this.currentlyLoadingGifs$,
    this.loadedGifs$,
  ]).pipe(
    map(([gifs, currentlyLoadingGifs, loadedGifs]) =>
      gifs.map((gif) => ({
        ...gif,
        loading: currentlyLoadingGifs.includes(gif.permalink),
        dataLoaded: loadedGifs.includes(gif.permalink),
      }))
    )
  );

  constructor(private redditService: RedditService) {}

  setLoading(permalink: string) {
    // Add the gifs permalink to the loading array
    this.currentlyLoadingGifs$.next([
      ...this.currentlyLoadingGifs$.value,
      permalink,
    ]);
  }

  setLoadingComplete(permalinkToComplete: string) {
    this.loadedGifs$.next([...this.loadedGifs$.value, permalinkToComplete]);

    this.currentlyLoadingGifs$.next([
      ...this.currentlyLoadingGifs$.value.filter(
        (permalink) => !this.loadedGifs$.value.includes(permalink)
      ),
    ]);
  }
}

To make things a bit cleaner to follow along with, we are going to delete everything that is currently in this file and we will add it back the Component Store way.

Modify home.store.ts to reflect the following:

import { Injectable } from '@angular/core';

@Injectable()
export class HomeStore {

}

Defining and initialising state

The first thing I like to do when setting up Component Store is to define the state that we want to store in it.

Modify home.store.ts to reflect the following:

import { Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';

interface HomeState {
  currentlyLoadingGifs: string[];
  loadedGifs: string[];
  settingsModalIsOpen: boolean;
}

@Injectable()
export class HomeStore extends ComponentStore<HomeState> {
  constructor() {
    super({
      currentlyLoadingGifs: [],
      loadedGifs: [],
      settingsModalIsOpen: false,
    });
  }
}

Notice that we create a HomeState interface that defines the state information we want to store. Then, we have our HomeStore extend ComponentStore and pass in the state interface we just defined as the type.

By using the extends keyword, we are making our HomeStore a sub-class of the ComponentStore class that @ngrx/component-store provides for us. This is how we have access to the functionality that ComponentStore provides.

We then have this super call in the constructor. If you are not familiar with inheritance in OOP, the super method is a way to call the constructor of the parent class (or “super class”). In this case, we are passing our initial state to the constructor of the ComponentStore class.

The state we have defined is just an object with simple values, not observables. We will still access this state as an observable stream, but it is Component Store that will handle this for us. We don’t need to manually create our own observables in this case.

Something you might have noticed is that whilst a gifs$ observable and a subredditFormControl were including in our initial HomeStore, they have not been included as part of our state with Component Store.

In the case of gifs, this is because it is derived/computed from other state information. We combine other parts of our state together to get the result for this, so it is not a core part of our state.

Now, technically our subredditFormControl does hold state information, but we aren’t using Component Store to manage it. A FormControl already has its own ways to keep track of and update its value. The only reason we are storing it within our store is that we need its value to derive our gifs value that we just mentioned.

Using State Information with Selectors

Now that we have state defined in our store, we need a way to access it.

Modify home.store.ts to reflect the following:

import { Injectable } from '@angular/core';
import { FormControl } from '@angular/forms';
import { ComponentStore } from '@ngrx/component-store';
import { RedditService } from '../../shared/data-access/reddit/reddit.service';

interface HomeState {
  currentlyLoadingGifs: string[];
  loadedGifs: string[];
  settingsModalIsOpen: boolean;
}

@Injectable()
export class HomeStore extends ComponentStore<HomeState> {
  subredditFormControl = new FormControl('gifs');

  currentlyLoadingGifs$ = this.select((state) => state.currentlyLoadingGifs);
  loadedGifs$ = this.select((state) => state.loadedGifs);
  settingsModalIsOpen$ = this.select((state) => state.settingsModalIsOpen);

  gifs$ = this.select(
    this.redditService.getGifs(this.subredditFormControl),
    this.currentlyLoadingGifs$,
    this.loadedGifs$,
    (gifs, currentlyLoadingGifs, loadedGifs) =>
      gifs.map((gif) => ({
        ...gif,
        loading: currentlyLoadingGifs.includes(gif.permalink),
        dataLoaded: loadedGifs.includes(gif.permalink),
      }))
  );

  constructor(private redditService: RedditService) {
    super({
      currentlyLoadingGifs: [],
      loadedGifs: [],
      settingsModalIsOpen: false,
    });
  }
}
EXTENDED
Key

Thanks for checking out the preview of this lesson!

You do not have the appropriate membership to view the full lesson. If you would like full access to this module you can view membership options (or log in if you are already have an appropriate membership).