Component Store

Preview — 3 of 10 questions

What is @ngrx/component-store and when should it be used over the global store?

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

interface PaginationState {
  items: Product[];
  currentPage: number;
  totalPages: number;
  loading: boolean;
  error: string | null;
}

@Injectable()  // NOT providedIn: 'root' — scoped to component
export class ProductListStore extends ComponentStore<PaginationState> {
  constructor(private productService: ProductService) {
    super({ items: [], currentPage: 1, totalPages: 0, loading: false, error: null });
  }

  // Selectors
  readonly items$ = this.select(state => state.items);
  readonly loading$ = this.select(state => state.loading);
  readonly vm$ = this.select(
    this.items$, this.loading$,
    (items, loading) => ({ items, loading }),  // derived ViewModel
  );

  // Updaters (synchronous state mutations)
  readonly setPage = this.updater((state, page: number) => ({
    ...state, currentPage: page,
  }));

  // Effects (async operations)
  readonly loadProducts = this.effect<number>(page$ =>
    page$.pipe(
      switchMap(page =>
        this.productService.getPage(page).pipe(
          tapResponse(
            response => this.patchState({
              items: response.items,
              totalPages: response.totalPages,
              loading: false,
            }),
            err => this.patchState({ error: err.message, loading: false }),
          ),
        )
      ),
    )
  );
}

// Component
@Component({
  selector: 'app-product-list',
  template: `
    @if (vm$ | async; as vm) {
      @for (item of vm.items; track item.id) {
        <app-product-card [product]="item" />
      }
    }
  `,
  providers: [ProductListStore],  // scoped instance
  standalone: true,
})
export class ProductListComponent {
  vm$ = inject(ProductListStore).vm$;
}
AA micro version of NgRx store with no actions or effects, suitable for simple services
BA store optimized for use with Angular signals only
CA store that uses localStorage for persistence by default
DA local state management solution scoped to a component or service, with lifecycle tied to the component, for state that is not shared app-wide

What does @ngrx/router-store enable?

javascript
// Setup:
bootstrapApplication(AppComponent, {
  providers: [
    provideStore({ router: routerReducer }),
    provideRouterStore({
      serializer: DefaultRouterStateSerializer,
    }),
  ],
});

// Or custom serializer for only what you need:
import { MinimalRouterStateSerializer } from '@ngrx/router-store';
provideRouterStore({ serializer: MinimalRouterStateSerializer })

// Selectors:
import { getRouterSelectors } from '@ngrx/router-store';

export const {
  selectCurrentRoute,
  selectFragment,
  selectQueryParams,
  selectQueryParam,
  selectRouteParams,
  selectRouteParam,
  selectRouteData,
  selectUrl,
  selectTitle,
} = getRouterSelectors();

// Usage in a selector:
export const selectProductId = selectRouteParam('id');

export const selectCurrentProduct = createSelector(
  selectProductId,
  selectProductEntities,
  (id, entities) => id ? entities[id] : null
);

// Usage in component:
currentUrl$ = this.store.select(selectUrl);
productId$ = this.store.select(selectProductId);
AConnecting the Angular Router state to the NgRx store — making router state (URL, params, query params) available as store state and dispatching router events as actions
BAutomatic generation of routes from NgRx actions
CCaching of component state across route navigations
DRoute-level access control driven by NgRx store values

How do you unit test an NgRx Effect?

javascript
import { TestBed } from '@angular/core/testing';
import { provideMockActions } from '@ngrx/effects/testing';
import { provideMockStore } from '@ngrx/store/testing';
import { cold, hot } from 'jest-marbles';  // or jasmine-marbles

describe('UsersEffects', () => {
  let effects: UsersEffects;
  let actions$: Observable<Action>;
  let userService: jest.Mocked<UserService>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        UsersEffects,
        provideMockActions(() => actions$),
        provideMockStore({ initialState: { users: [] } }),
        {
          provide: UserService,
          useValue: { getUsers: jest.fn() },
        },
      ],
    });

    effects = TestBed.inject(UsersEffects);
    userService = TestBed.inject(UserService) as jest.Mocked<UserService>;
  });

  it('should dispatch loadUsersSuccess on successful API call', () => {
    const users: User[] = [{ id: '1', name: 'Alice' }];

    // Use marble testing for cold/hot Observables:
    actions$ = hot('-a', { a: loadUsers() });
    userService.getUsers.mockReturnValue(cold('-b|', { b: users }));

    expect(effects.loadUsers$).toBeObservable(
      hot('--c', { c: loadUsersSuccess({ users }) })
    );
  });

  it('should dispatch loadUsersFailure on API error', () => {
    actions$ = hot('-a', { a: loadUsers() });
    userService.getUsers.mockReturnValue(cold('-#', {}, new Error('Network Error')));

    expect(effects.loadUsers$).toBeObservable(
      hot('--c', { c: loadUsersFailure({ error: 'Network Error' }) })
    );
  });
});
AUse TestBed with StoreModule.forRoot() and dispatch real actions
BUse provideMockActions() and provideMockStore() in TestBed, providing an Observable<Action> for the actions stream
CCall the Effect method directly as a function
DUse SpyOn the Actions service and mock its pipe method

Sign up free to play

Answer all 10 questions (7 more), see explanations for every answer, and track your score.