Skillfully Using Signals in Angular – Selected Hints for Professional Use

  1. Signals in Angular: Building Blocks
  2. Component Communication with Signals: Inputs, Two-Way Bindings, and Content/ View Queries
  3. Successful with Signals in Angular – 3 Effective Rules for Your Architecture
  4. Skillfully Using Signals in Angular – Selected Hints for Professional Use
  5. When (Not) to use Effects in Angular — and what to do instead
  6. Asynchronous Data Flow with Angular’s new Resource API
  7. Streaming Resources in Angular – Details and Semantics
  8. Streaming Resources for a Chat with Web Sockets: Messages in a Glitch-Free World
  9. Angular’s new httpResource

The new Signals in Angular are a simple reactive building block. However, as often, the devil is in the details. In this article, I'll give two hints to help you use Signals more straightforwardly.

📂 Source Code

Guiding Theory: Unidirectional Data Flow With Signals

The approach to establishing a unidirectional data flow presented in the last article serves as the guiding theory for the two hints presented here:

Unidirectional data flow with a store

Handlers for UI events delegate to the store. I use the abstract term intention for this, as different stores have completely different implementations for this: In the Redux-based NGRX Store, Actions are dispatched; in the lightweight NGRX Signal Store, however, the component calls a method offered by the store.

The store then executes synchronous or asynchronous tasks. These usually lead to a change in state, which the application transports to the views of the individual components via signals. As part of this data flow, the state can be projected onto View Models using computed, i.e., onto data structures that represent the view of individual use cases on the state.

This approach is based on the fact that Signals are primarily suitable for informing the view synchronously about data and data changes. They are less suitable for asynchronous tasks and for representing events: Firstly, they do not offer an easy way to deal with overlapping asynchronous requests and the resulting race conditions. In addition, they cannot directly represent error states. Secondly, Signals ignore the resulting intermediate states when value changes occur in direct succession. This desired property is called glitch-free.

For example, if a Signal changes from 1 to 2 and immediately afterwards from 2 to 3, the consumer only receives a notification about the 3. This is also beneficial for data binding performance, especially since updating with intermediate results would result in unnecessary tasks.

Hint 1: Signals Harmonize with RxJS

Signals are deliberately kept simple. That's why they offer fewer options than RxJS, which has been established in the Angular world for years. Thanks to the RxJS interop that Angular brings, you can combine the best of both worlds. The following listing demonstrates this:

@Component({
  selector: 'app-desserts',
  standalone: true,
  imports: [DessertCardComponent, FormsModule, JsonPipe],
  templateUrl: './desserts.component.html',
  styleUrl: './desserts.component.css',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DessertsComponent {
  #dessertService = inject(DessertService);
  #ratingService = inject(RatingService);
  #toastService = inject(ToastService);

  originalName = signal('');
  englishName = signal('Cake');
  loading = signal(false);

  ratings = signal<DessertIdToRatingMap>({});
  ratedDesserts = computed(() => this.toRated(this.desserts(), this.ratings()));

  originalName$ = toObservable(this.originalName);
  englishName$ = toObservable(this.englishName);

  desserts$ = combineLatest({
    originalName: this.originalName$,
    englishName: this.englishName$,
  }).pipe(
    filter((c) => c.originalName.length >= 3 || c.englishName.length >= 3),
    debounceTime(300),
    tap(() => this.loading.set(true)),
    switchMap((c) =>
      this.#dessertService.find(c).pipe(
        catchError((error) => {
          this.#toastService.show('Error loading desserts!');
          console.error(error);
          return of([]);
        }),
      ),
    ),
    tap(() => this.loading.set(false)),
  );

  desserts = toSignal(this.desserts$, {
    initialValue: [],
  });

  […]
}

This example converts the Signals from and to into Observables and implements a typeahead based on this. For this, it uses the filter , debounceTime and switchMap operators offered by RxJS. The latter also prevents race conditions resulting from overlapping requests by only respecting the most recent request. All other still running requests are cancelled.

At the end, the resulting Observable is converted to a Signal so that the application can continue with the new Signals API. Of course, for performance reasons, the application should not switch between the two worlds too often.

Unlike in the figure above, no store is used here. Both the intention and the asynchronous action take place in the reactive data flow. If you move the data flow to a Service that shares the loaded data, e.g., with shareReplay, you could already see this service as a simple store. However, like in the discussed figure, the component delegates the execution of an asynchronous task to somewhere else and gets the resulting state via Signals.

RxJS in Stores

RxJS is also often used in stores. For instance, the traditional NGRX store uses RxJS for Actions and Effects. As an alternative to Effects, the NGRX Signal Store offers reactive methods that can be defined with rxMethod:

export const DessertStore = signalStore(
  { providedIn: 'root' },
  withState({
    filter: {
      originalName: '',
      englishName: 'Cake',
    },
    loading: false,
    ratings: {} as DessertIdToRatingMap,
    desserts: [] as Dessert[],
  }),
  […]
  withMethods(
    (
      store,
      dessertService = inject(DessertService),
      toastService = inject(ToastService),
    ) => ({

      […]
      loadDessertsByFilter: rxMethod<DessertFilter>(
        pipe(
          filter(
            (f) => f.originalName.length >= 3 || f.englishName.length >= 3,
          ),
          debounceTime(300),
          tap(() => patchState(store, { loading: true })),
          switchMap((f) =>
            dessertService.find(f).pipe(
              tapResponse({
                next: (desserts) => {
                  patchState(store, { desserts, loading: false });
                },
                error: (error) => {
                  toastService.show('Error loading desserts!');
                  console.error(error);
                  patchState(store, { loading: false });
                },
              }),
            ),
          ),
        ),
      ),
    }),
  ),
  withHooks({
    onInit(store) {
      const filter = store.filter;
      store.loadDessertsByFilter(filter);
    },
  }),
);

This example sets up a reactive loadDessertsByFilter method in the store. Since it is defined with rxMethod<DessertFilter>, it accepts an Observable<DessertFilter>. The values of this Observable pass through the specified pipe. Since rxMethod automatically subscribes this Observable, the application code must receive the result of the data flow using tap or tabResponse. The latter is an operator from the ngrx/operators package combining the functionality of tap, catchError, and finalize.

The consumer of a reactive method can pass a corresponding Observable, but also a Signal or a concrete value. For example, the onInit hook shown passes the filter Signal. This means that all values that the Signal gradually receives pass through the pipe in loadDessertsByFilter. respecting the above-mentioned glitch-free property.

Interestingly, rxMethod can also be used outside of the Signal Store by design. For example, a component could use it to set up a reactive method.

Angular Architecture Workshop (online, interactive, advanced)

Become an expert for enterprise-scale and maintainable Angular applications with our Angular Architecture workshop!

All Details (English Workshop) | All Details (German Workshop)

Hint 2: Avoiding Race Conditions

Overlapping asynchronous operations usually lead to undesirable race conditions. For example, if the user searches for two different desserts in quick succession, both results might be displayed one after the other. One of the two only flashes briefly before the other replaces it. Due to the asynchronous nature, the order of results does not need to match the order of requests.

To prevent this confusing behavior, RxJS provides various flattening operators:

  • switchMap
  • mergeMap
  • concatMap
  • exhaustMap

These operators differ in how they handle overlapping requests. The switchMap already discussed above always only returns the result of the last search query if there are several overlapping ones. Any requests that may already be in progress are canceled when a new request arrives. This behavior is what users intuitively expect when working with search filters.

The mergeMap and concatMap operators perform all queries: the former in parallel and the latter sequentially. The exhaustMap operator ignores further requests while one is running. These capabilities are another reason to use RxJS, as well as the previously discussed RxJS interop and rxMethod.

Another strategy that is often used in addition or as an alternative is a flag that informs whether the application is currently communicating with the backend:

loadRatings(): void {
  patchState(store, { loading: true });

  ratingService.loadExpertRatings().subscribe({
    next: (ratings) => {
      patchState(store, { ratings, loading: false });
    },
    error: (error) => {
      patchState(store, { loading: false });
      toastService.show('Error loading ratings!');
      console.error(error);
    },
  });
},

Depending on the value of this flag, the application could display a loading indicator or disable the respective button. The latter is of course counterproductive with a highly reactive UI or not possible at all if the application does not have an explicit button.

Summary

RxJS and Signals work beautifully together and the RxJS interop offered by Angular gives us the best of both worlds. I'd recommend using RxJS to represent events. For processing asynchronous tasks, RxJS or stores, which may be based on RxJS, are a good fit. However, Signals should take over the synchronous transport of the received data into the view. Together, RxJS, Stores, and Signals are building blocks for establishing a unidirectional dataflows.

In addition, the flattening operators in RxJS are elegant solutions for preventing race conditions. Alternatively or in addition, flags can also be used to indicate whether a request is currently in progress at the backend.

eBook: Micro Frontends and Moduliths with Angular

Learn how to build enterprise-scale Angular applications which are maintainable in the long run

✓ 20 chapters
✓ source code examples
✓ PDF, epub (Android and iOS) and mobi (Kindle)

Get it for free!