3d cartoon hands holding a phone

Unlock full course by purchasing a membership

Lesson 5

Using Component Store for Global State

A lightweight solution to global state management

EXTENDED

Using Component Store for Global State

Typically, the use case for @ngrx/component-store is for state that is local to a single component. That means the state is not being consumed throughout the application, it is just being used by the individual component that the store is provided to.

One way we have thought about Component Store is that it is pretty much like the standard service with a subject approach that we have been using and discussing - where we just create a service and that service has a BehaviorSubject in it. Using Component Store is similar to this… it’s just a bit more robust and has some extra features built in.

In multiple of the applications we have built, we have been using a service with a subject to manage global state. If a Component Store is similar to the service with a subject approach, but more robust, then why shouldn’t we also be able to use Component Store for global state?

The answer is: we can!

There are no rules or reasons preventing this, and it is even a good idea to do this. NgRx also has a state management solution specifically for global state called @ngrx/store. But, this is significantly more complex than @ngrx/component-store. Although you can certainly learn and use @ngrx/store if you want, for the vast majority of state management use cases the more simplistic approach of @ngrx/component-store will be enough.

What does it look like?

There is nothing new to learn here. To use a ComponentStore for global state management, all we need to do is provide it at the root level:

@Injectable({
    providedIn: 'root'
})

Typically, when using it for local state, we do not use providedIn and we provide the store directly to a component in the components metadata. To use the store globally, we use providedIn: 'root'. Then, we can just inject it into any component we want through the constructor as we typically do for services.

This way, any component or service that wants to use this store will share the state with any other component that wants to use it.

Refactoring to use Component Store for Global State

Let’s make this concept a little more concrete by doing another hypothetical refactor. In the Quicklists application we built, we had a shared service called ChecklistService. The purpose of this service was to manage the CRUD (Create, Read, Update, Delete) operations related to the checklist data and it was shared throughout the application.

It looked like this:

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { filter, map, shareReplay, take, tap } from 'rxjs/operators';
import { ChecklistItemService } from '../../checklist/data-access/checklist-item.service';
import { AddChecklist, Checklist } from '../interfaces/checklist';
import { StorageService } from './storage.service';

@Injectable({
  providedIn: 'root',
})
export class ChecklistService {
  // This is our data source where we will emit the latest state
  private checklists$ = new BehaviorSubject<Checklist[]>([]);

  // This is what we will use to consume the checklist data throughout the app
  private sharedChecklists$: Observable<Checklist[]> = this.checklists$.pipe(
    // Trigger a save whenever this stream emits new data
    tap((checklists) => this.storageService.saveChecklists(checklists)),
    // Share this stream with multiple subscribers, instead of creating a new one for each
    shareReplay(1)
  );

  constructor(
    private storageService: StorageService,
    private checklistItemService: ChecklistItemService
  ) {}

  load() {
    this.storageService.loadChecklists$
      .pipe(take(1))
      .subscribe((checklists) => {
        this.checklists$.next(checklists);
      });
  }

  getChecklists() {
    return this.sharedChecklists$;
  }

  getChecklistById(id: string) {
    return this.getChecklists().pipe(
      filter((checklists) => checklists.length > 0), // don't emit if checklists haven't loaded yet
      map((checklists) => checklists.find((checklist) => checklist.id === id))
    );
  }

  add(checklist: AddChecklist) {
    const newChecklist = {
      ...checklist,
      id: this.generateSlug(checklist.title),
    };

    this.checklists$.next([...this.checklists$.value, newChecklist]);
  }

  remove(id: string) {
    const modifiedChecklists = this.checklists$.value.filter(
      (checklist) => checklist.id !== id
    );

    this.checklistItemService.removeAllItemsForChecklist(id);

    this.checklists$.next(modifiedChecklists);
  }

  update(id: string, editedData: AddChecklist) {
    const modifiedChecklists = this.checklists$.value.map((checklist) =>
      checklist.id === id
        ? { ...checklist, title: editedData.title }
        : checklist
    );

    this.checklists$.next(modifiedChecklists);
  }

  private generateSlug(title: string) {
    // NOTE: This is a simplistic slug generator and will not handle things like special characters.
    let slug = title.toLowerCase().replace(/s+/g, '-');

    // Check if the slug already exists
    const matchingSlugs = this.checklists$.value.find(
      (checklist) => checklist.id === slug
    );

    // If the title is already being used, add a string to make the slug unique
    if (matchingSlugs) {
      slug = slug + Date.now().toString();
    }

    return slug;
  }
}
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).