Easier Angular Component State Management

0
101

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:

  1. Difficulty Scaling: Your component quickly becomes cluttered with member variables and messy templates.
  2. 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 Basalhttps://netbasal.com/a-comprehensive-guide-to-angular-onpush-change-detection-strategy-5bac493074a4

Solution

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
Loading...
`, changeDetection: ChangeDetectionStrategy.OnPush, ) export class MyComponent implements OnInit state$ = new BehaviorSubject( 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:

  1. Data is loading
  2. Data is found
  3. Data is not found

👍 The Upsides

  1. 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);
  

Where RxJSBaseClass is:

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

Note: 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: `
    

Conclusion

Learning how Angular’s change detection works will give you insight into how you can better handle your component’s state.

Thanks for reading!

Angular 10

Ionic 5 Google Map