The NgRx Component Store is neat!

popefelix

Aurelia Peters

Posted on December 18, 2023

The NgRx Component Store is neat!

See this project on Stackblitz

Lately I've been messing around with the NgRx Component Store, which is a sort of stripped down version of their Store software. The central idea is that your Angular application (or in my case, component) has a central data store class that manages the application / component state. What is "state" in this context? It's all the data that your application / component works with. The larger application that I'm currently working on uses worker processes to handle certain long-running jobs, so in my case, the state of my component is the execution status of those jobs, as reported by an HTTP backend.

A job status record has the following fields:

  • worker - the name of the worker that is recording this status entry. For example, if you had a worker that was transcoding a video stream, you might call it "transcode"
  • job - The name of the job the worker is executing. Using the previous example of transcoding a video stream, you might call it "home-movie-1994-11-17.mpg".
  • jobType - This used to give more information about the job. Using the previous example, you might use "mpg_to_mp4"
  • jobStatus - This is used to record the status of the job. For the previous example, you might could put a completion percentage here. You could also use something like "Received", "In progress", "Complete" for jobs that aren't easily broken down into percent complete.
  • error - This is an optional field, only populated when something's gone wrong with the job. It will contain a (hopefully useful) error message.

To display these job status records, let's put them in a table.
Before I learned about NgRx, I would have gone about writing this component by first writing a service class that mediated the interaction with the backend. I then would have generated a component that interacted with the service class and fed data to the HTML template. If I wanted to be able to filter my results, I would have added form fields to the HTML template and added listeners on each form field's onChange event. The component class would be fairly large in terms of lines of code with all these listeners and whatnot, and I'd have to do some jiggery-pokery with RxJs to get everything plumbed up properly. I would also have had to manage the Subscriptions for each form field to make sure they all got unsubscribed whenever the component was destroyed. And that's all stuff I've done before, but it's kind of a lot.

With NgRx, however, this all becomes much easier. I create the service class as before, but then I create a component store. This component store is going to manage all of the data for my component. If I want the data laid out a certain way (in my case, I wanted a unique list of worker names, job IDs, job types, and job statuses), the code to do that will be in the component store.

The most important thing in this class is the definition of WorkerJobStatusState:

export interface WorkerJobStatusState {
  statuses: WorkerJobStatus[];
  worker: string[];
  job: string[];
  jobType: string[];
  jobStatus: string[];
}
Enter fullscreen mode Exit fullscreen mode

This encapsulates the entire state of the component at any given time. Notice how it doesn't just have the list of status records. The values of each of the filters are part of the state as well.

The real beauty of a component store, I think, comes in the selectors, which are pure functions providing a custom view of the component state. They can be composed together, as shown in the selectedStatuses$ selector, below:

readonly selectedStatuses$ = this.select(
    this.statuses$,
    this.selectedWorker$,
    this.selectedJob$,
    this.selectedJobType$,
    this.selectedJobStatus$,
    (
      statuses,
      selectedWorkers,
      selectedJobs,
      selectedJobTypes,
      selectedJobStatuses
    ) =>
      statuses.filter(
        (status) =>
          (selectedWorkers.length
            ? selectedWorkers.includes(status.worker)
            : true) &&
          (selectedJobs.length ? selectedJobs.includes(status.job) : true) &&
          (status.jobType && selectedJobTypes.length
            ? selectedJobTypes.includes(status.jobType)
            : true) &&
          (selectedJobStatuses.length
            ? selectedJobStatuses.includes(status.jobStatus)
            : true)
      )
  );
Enter fullscreen mode Exit fullscreen mode

20 lines of code handles everything around the status records. We take in the list of statuses provided by the backend service and the values of the filters and we return a list of matching statuses.

Now let's look at the component class for the status list. Notice how the only external class it exchanges data with is the store, and notice how it doesn't do any massaging / modification / filtering of the data the store provides. All of that is handled by the store.

We need to connect the filter form fields to the store, and we do that with these updater methods, which are methods provided by the component store allowing the caller to update the state.

    this._workerJobStatusStore.workersSelected(
      this.selectedWorker.valueChanges
    );
    this._workerJobStatusStore.jobsSelected(this.selectedJob.valueChanges);
    this._workerJobStatusStore.jobTypesSelected(
      this.selectedJobType.valueChanges
    );
    this._workerJobStatusStore.jobStatusesSelected(
      this.selectedJobStatus.valueChanges
    );
Enter fullscreen mode Exit fullscreen mode

Note that all we have to do in the way of plumbing is to pass the valueChanges Observable from each of the filtering form fields to the updater functions. NgRx will handle everything else for us, including unsubscribing when the component that instantiated the store is destroyed.

Now, if you look at the component code, you will notice that it doesn't do anything to connect the component store to the backend service. The component has no idea there even is a backend service, and that's intentional. The worker job status list component is intended to be purely presentational. The data store is provided by dependency injection, but how, you may ask, does the store get connected to the backend service? That gets handled in the container component:

The reason I use a container component here is so that I have a single module that provides the data store and populates the state. If I want to add a status detail component later, one that maybe would provide the logs for each job, I could add another presentational component to do that, but the data store would still be provided by the container.

OK, so I've added a lot of extra abstraction to what could have been a fairly simple component. Why? What good will this do me? I'll tell you:

  • Testability - by abstracting the data interaction into the data store class, I can test those methods without having to worry about setting up the component in my test harness or mocking the backend service
  • Maintainability - By centralizing common functionality into a single class, I don't have to (for example) populate the data store for every component.
  • Extensibility - It's easy to add a new component that uses the same data store.

And I think it makes the code more readable. The job status list component only handles displaying the list. The container component only handles populating the data store. The data store class only handles viewing / modifying the state. Sure, this was a fairly simple example, but for more complex components, this could be invaluable.

So what do you think? Have you worked with component store / NgRx at all? What were your experiences like?

💖 💪 🙅 🚩
popefelix
Aurelia Peters

Posted on December 18, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related