Enterprise Angular

State Management


The Goal

We will discuss the importance of state management and why it is one of the most critical pieces to get right from an architecture standpoint.

We will show how NgRx neatly solves a lot of state management problems while demystifying some of the common misconceptions of the framework.

We will talk about how the facade pattern has become the villian when, in fact, it is a great mechanism for decoupling your application at scale.

Our goal is to wire up a feature using NgRx

Our goal is to implement the facade pattern in a way that leverages other state management options without the component layer knowing.

The Code

The State Layer

Create the feature libraries to hold state management.

npx nx g @nx/angular:library users-state --directory=libs/users-state --standalone=false --projectNameAndRootFormat=as-provided
npx nx g @nx/angular:library challenges-state --directory=libs/challenges-state --standalone=false --projectNameAndRootFormat=as-provided
npx nx g @nx/angular:library flashcards-state --directory=libs/flashcards-state --standalone=false --projectNameAndRootFormat=as-provided
npx nx g @nx/angular:library notes-state --directory=libs/notes-state --standalone=false --projectNameAndRootFormat=as-provided
npx nx g @nx/angular:library features-state --directory=libs/features-state --standalone=false --projectNameAndRootFormat=as-provided

Generate the NgRx code for the users feature.

npx nx g @nx/angular:ngrx-feature-store --name=users \
  --directory=state \
  --parent=libs/users-state/src/lib/users-state.module.ts \
  --route=apps/users/src/app/remote-entry/entry.routes.ts \
  --facade=true

Generate the NgRx code for the challenges feature.

npx nx g @nx/angular:ngrx-feature-store --name=challenges \
  --directory=state \
  --parent=libs/challenges-state/src/lib/challenges-state.module.ts \
  --route=apps/challenges/src/app/remote-entry/entry.routes.ts \
  --facade=true

Generate the NgRx code for the flashcards feature.

npx nx g @nx/angular:ngrx-feature-store --name=flashcards \
  --directory=state \
  --parent=libs/flashcards-state/src/lib/flashcards-state.module.ts \
  --route=apps/flashcards/src/app/remote-entry/entry.routes.ts \
  --facade=true

Generate the NgRx code for the notes feature.

npx nx g @nx/angular:ngrx-feature-store --name=notes \
  --directory=state \
  --parent=libs/notes-state/src/lib/notes-state.module.ts \
  --route=apps/notes/src/app/remote-entry/entry.routes.ts \
  --facade=true

Generate the NgRx code for the features feature.

npx nx g @nx/angular:ngrx-feature-store --name=features \
  --directory=state \
  --parent=libs/features-state/src/lib/features-state.module.ts \
  --route=apps/portal/src/app/remote-entry/entry.routes.ts \
  --facade=true
export const appConfig: ApplicationConfig = {
  providers: [
    provideEffects(),
    provideStore(
      {
        router: routerReducer,
      },
      {
        runtimeChecks: {
          strictStateImmutability: true,
          strictActionImmutability: true,
          strictStateSerializability: true,
          strictActionSerializability: true,
          strictActionWithinNgZone: true,
          strictActionTypeUniqueness: true,
        },
      }
    ),
    provideRouterStore(),
    provideStoreDevtools({
      maxAge: 25,
      logOnly: !isDevMode(),
    }),
  ],
};
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ChallengesComponent } from '../challenges/challenges.component';

@Component({
  standalone: true,
  imports: [CommonModule, ChallengesComponent],
  selector: 'proto-challenges-entry',
  template: `<proto-challenges></proto-challenges>`,
})
export class RemoteEntryComponent {}
import { Route } from '@angular/router';
import { provideEffects } from '@ngrx/effects';
import { provideState } from '@ngrx/store';
import { ChallengesEffects, ChallengesState } from '@proto/challenges-state';
import { RemoteEntryComponent } from './entry.component';

export const remoteRoutes: Route[] = [
  {
    path: '',
    component: RemoteEntryComponent,
    providers: [
      provideEffects(ChallengesEffects),
      provideState(ChallengesState.CHALLENGES_FEATURE_KEY, ChallengesState.reducers),
    ],
  },
];
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { Challenge } from '@proto/api-interfaces';
import { MaterialModule } from '@proto/material';
import { ChallengesFacade } from '@proto/challenges-state';
import { Observable, filter } from 'rxjs';
import { ChallengeDetailsComponent } from './challenge-details/challenge-details.component';
import { ChallengesListComponent } from './challenges-list/challenges-list.component';

@Component({
  selector: 'proto-challenges',
  standalone: true,
  imports: [
    CommonModule,
    MaterialModule,
    ChallengesListComponent,
    ChallengeDetailsComponent,
  ],
  templateUrl: './challenges.component.html',
  styleUrls: ['./challenges.component.scss'],
})
export class ChallengesComponent implements OnInit {
  challenges$: Observable<Challenge[]> = this.challengesFacade.allChallenges$;
  selectedChallenge$: Observable<Challenge> = this.challengesFacade.selectedChallenge$.pipe(
    filter((challenge): challenge is Challenge => challenge !== undefined && challenge !== '')
  );

  constructor(private challengesFacade: ChallengesFacade) {}

  ngOnInit(): void {
    this.reset();
  }

  reset() {
    this.loadChallenges();
    this.challengesFacade.resetSelectedChallenge();
  }

  selectChallenge(challenge: Challenge) {
    this.challengesFacade.selectChallenge(challenge.id as string);
  }

  loadChallenges() {
    this.challengesFacade.loadChallenges();
  }

  saveChallenge(challenge: Challenge) {
    this.challengesFacade.saveChallenge(challenge);
  }

  deleteChallenge(challenge: Challenge) {
    this.challengesFacade.deleteChallenge(challenge);
  }
}
< Angular Home