Managing the state of Angular components can become complex as your component grows. Note: As of the ngrx version 10 release, it is now recommended to use
@ngrx/component-store for easier Angular component state management. A simple approach to handle your component’s state is with component member variables and Angular’s default change detection. However, this approach is flawed for two reasons:
- Difficulty Scaling: Your component quickly becomes cluttered with member variables and messy templates.
- Performance Issues: You lack control over Angular’s change detection, ultimately hurting your application’s performance. This is because default change detection will run multiple times whenever a change is detected within the component: e.g. user events, timers, XHR, promises, etc. As an application grows, minimizing change detection code is crucial for a fast application that your users love.
To handle these issues, we can house the component’s state in a single
BehaviorSubject and use Angular’s built-in
async template pipe and
OnPush change detection to increase performance.
To learn more about
OnPush change detection, I recommend reading this article by Netanel Basal: https://netbasal.com/a-comprehensive-guide-to-angular-onpush-change-detection-strategy-5bac493074a4
Your component has one member variable called
state$ that houses the component’s state and then uses an
async pipe wrapper in the template to feed data into the template:
interface State null; loaded: boolean; error: any; @Component( async as state">
data Data not found! An Error occurred: error `, changeDetection: ChangeDetectionStrategy.OnPush, ) export class MyComponent implements OnInit state$ = new BehaviorSubjectLoading... ( data: null, loaded: false, error: null, ); constructor(private store: Store) ngOnInit() this.store.selectData().subscribe( (data) => this.state$.next( data, loaded: true, error: null, ); , (error) => this.state$.next( data: null, loaded: true, error, ); );
With this code, you can easily manage the basic states that 90% of your components use:
- Data is loading
- Data is found
- Data is not found
👍 The Upsides
- Cleaner templates: No more nesting
ng-containers that make the template cluttered and buggy
The inner data observables will not be subscribed to until the outer ones are subscribed to causing much confusion and bugs!
2. Fine-grain control: We choose when to update the component’s state and change detection.
👎 The Downsides
Of course, no solution is perfect. Because we only have one
BehaviorSubject handling the state, which means we have to manually handle external subscriptions inside the component’s Typescript file.
Not to fear, I have provided a full solution to this problem with the code below. Feel free to use this abstract class throughout your application! 😁
import Directive from '@angular/core'; import RxJSBaseClass from '@involvemint/shared/utils'; import BehaviorSubject, Observable from 'rxjs'; @Directive() export abstract class StatefulComponent
extends RxJSBaseClass /** Public/Template facing observable */ public readonly state$: Observable ; /** Component State BehaviorSubject */ private readonly _state$: BehaviorSubject ; /** Initial Component state stored in the case of calling `resetState` */ private readonly _initState: State; constructor(protected readonly initState: State) super(); this._initState = initState; this._state$ = new BehaviorSubject (initState); this.state$ = this._state$.asObservable(); /** Current Component State */ protected get state(): State return this._state$.value; /** Set New Component State */ protected setState(newState: State): void this._state$.next(newState); /** Update State partially and keep non-inputted fields the same */ protected updateState(newState: Partial ): void this._state$.next( ...this.state, ...newState ); /** Reset State back to initial state given in class constructor */ protected resetState(): void this.setState(this._initState);
import Directive, OnDestroy from '@angular/core'; import Subject from 'rxjs'; @Directive() export abstract class RxJSBaseClass implements OnDestroy private _destroy$!: Subject
; public get destroy$(): Subject if (!this._destroy$) // Performance optimization: // Since this is likely used as base component everywhere, // only construct a Subject instance if actually used. this._destroy$ = new Subject(); return this._destroy$; ngOnDestroy(): void if (this._destroy$) this._destroy$.next(); this._destroy$.complete();
RxJSBaseClass is separate so you can extend it to Angular services.
Here is the example code implemented with this full-solution:
interface State null; loaded: boolean; error: any; @Component( selector: 'my-component', template: `
implements OnInit constructor(private store: Store) super( data: null, loaded: false, error: null, ); ngOnInit() this.store .selectData() .pipe(takeUntil(this.destroy$)) .subscribe( (data) => this.setState( data, loaded: true, error: null, ); , (error) => this.setState( data: null, loaded: true, error, ); );
Learning how Angular’s change detection works will give you insight into how you can better handle your component’s state.
Thanks for reading!