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);
}
}