Tech Blog: Mastering Component State Management in Angular with the Signal API

Managing the state of components is a common challenge for frontend developers. This often includes scenarios like displaying a preloader during asynchronous actions or showing an error message upon their failure.

In this article, we will explore several strategies for managing component states effectively using Angular and the innovative Signal API.

Understanding State in Angular Components

In many Angular applications, the state of an API request is often stored in a component variable to manage various UI states. For instance:

@Component({

selector: ‘app-example’,

template: `

@if (loading()) {

Loading…

}

@if (error()) {

Error: {{ error() }}

}

@if (data()) {

{{ data() }}

}

`

})

export class ExampleComponent {

readonly loading = signal(false);

readonly error = signal<Error | null>();

data = null;

private readonly api = inject(ApiService);

constructor() {

this.loading.set(true);

this.api.getData().subscribe(

data => this.data.set(data),

error => this.error.set(error),

() => this.loading.set(false)

);

}

}

While this approach is functional, it might not always be optimal. Instead of coupling state management to component variables, we could derive state directly from the requests themselves. This should be particularly useful in scenarios where different component states do not necessarily correlate, such as simultaneous data loading and API updates.

Handling Asynchronous Actions

1. Streaming Data

For data that is frequently updated (like those fetched with GET requests), Angular’s observables are the ideal choice. This is because observables are cancelable and cancelling an obsolete GET request can save our backend resources.

In addition we could enhance these observables by using a custom operator that appends state information to the data stream:

import { EMPTY, Observable, OperatorFunction } from ‘rxjs’;

import { catchError, map, startWith, switchMap, tap } from ‘rxjs/operators’;

 

// Define a type for stateful observables

export interface State<T, E = Error> {

data: T | null;

error: E | null;

pending: boolean;

}

 

// Custom operator to add state to an observable

export function withState<T, E = Error>(retry: Observable<unknown> = EMPTY): OperatorFunction<T, State<T, E>> {

let state: State<T, E> = { data: null, error: null, pending: false };

 

return (source: Observable<T>) =>

retry.pipe(

startWith(null),

switchMap(() =>

source.pipe(

map(data => ({ data, pending: false })),

catchError(error => of({ error, pending: false })),

startWith({ error: null, pending: true }),

tap(updatedState => (state = { …state, …updatedState })),

map(() => state),

),

),

);

}

This stateful operator wraps an observable to manage data reloads effectively, this way we can maintain the previous state until new data is fully loaded.

Here is how you can use it:

import { Component } from ‘@angular/core’;

import { toSignal } from ‘@angular/core/rxjs-interop’;

 

@Component({

selector: ‘app-example’,

template: `

@if (data(); as state) {

@if (state.pending) {

Loading…

}

@if (state.error) {

Error: {{ state.error.message }}

}

@if (state.data) {

{{ state.data }}

}

}

`

})

export class ExampleComponent {

readonly data = toSignal(this.api.getData().pipe(stateful()));

 

private readonly api = inject(ApiService);

}

 

2. Action-Based Data Changes

For actions like POST, PUT, or DELETE, where a distinct start and end state are essential, managing state through an Observable may not be optimal. Because even if we cancel the previous request, it’s possible that the record has already been deleted, modified, or added to the database. Instead, creating a stateful function using promises and the Signal API is more suited:

import { signal, Signal } from ‘@angular/core’;

 

export interface StatefulFn<A extends unknown[] = unknown[], R = void> {

pending: Signal<boolean>;

error: Signal<Error | null>;

 

(…args: A): Promise<R>;

}

 

export function createStatefulFn<A extends unknown[], R>(origin: (…args: A) => Promise<R>): StatefulFn<A, R> {

const fn = async function(…args: A) {

fn.pending.set(true);

fn.error.set(null);

 

try {

return await origin(…args);

} catch (error: unknown) {

fn.error.set(error as Error);

throw error;

} finally {

fn.pending.set(false);

}

};

 

fn.pending = signal(false);

fn.error = signal<Error | null>(null);

 

return fn;

}

This technique allows components to initiate asynchronous actions while being able to display state updates directly within the component template.

Here is how you can use it:

import { Component } from ‘@angular/core’;

import { createStatefulFn } from ‘./stateful-fn’;

 

@Component({

selector: ‘app-example’,

template: `

<form>

<button (click)=”submit(‘Laptop’)” [disabled]=”submit.pending()”>Add product</button>

 

@if (submit.error(); as error) {

Error: {{ error.message }}

}

</form>

`

})

export class ExampleComponent {

readonly submit = createStatefulFn(async (name: string) => {

await this.api.addProduct(name);

});

 

private readonly api = inject(ApiService);

}

 

Conclusion

The methods discussed provide a robust framework for managing component states in Angular applications. These techniques simplify state management and enhance the responsiveness and clarity of the user interface. While we’ve focused on specific instances, the principles can be applied to broader scenarios, potentially involving services or local storage.

For a practical demonstration, you can view these examples in action on StackBlitz:

Stateful Observable Example
[Stateful Function Example](https://stackblitz.com/edit/stateful-fn?embed=1&file=src%2F

 

ezgif.com animated gif maker (5)

 

Other blogs

A Guide to i-Con Conference 2024: Your Passport to Limassol’s Best

16 May 2024
Read more about

2024 AI Tools to Boost Affiliate Campaign Performance

26 April 2024
Read more about