Logo

dev-resources.site

for different kinds of informations.

How I structure my Angular components with Signals

Published at
10/8/2024
Categories
angular
javascript
rxjs
Author
michelestieven
Categories
3 categories in total
angular
open
javascript
open
rxjs
open
Author
14 person written this
michelestieven
open
How I structure my Angular components with Signals

In this short article I want to show you how I like to structure my components with signals, with no external library. Of course things like NgRx would play a huge role to make our code more robust, but let's start simple!

First of all I define all of my states with signals:

export class TodoListComponent {
  todos = signal<Todo[]>([]);
}
Enter fullscreen mode Exit fullscreen mode

The same applies for inputs, too! If my component needs an input, I declare it with the new input() function by Angular, which gives me a signal as well. And if it happens to be a route parameter, I use input.required().

Then, if I want to show some state that can be derived from another one, I always use computed:

completedTodos = computed(() => this.todos().filter(t => t.completed));
Enter fullscreen mode Exit fullscreen mode

Then, if you know me, you know how much I despise performing asynchronous side-effects directly inside class methods... 🤢

export class TodoListComponent {
  todoService = inject(TodoService);

  toggleTodo(id: string) {
    this.todoService.toggle(id).subscribe(newTodo => ...);
  }
}
Enter fullscreen mode Exit fullscreen mode

Why you ask? Because if the method is the one who directly initiates the side-effect (in this case, calling subscribe), you have no control over back-pressure.

Back-pressure can be summed up with this: what happens if the user toggles the todo while the previous call hasn't finished?

There are a number of problems, for example:

  1. Do we even want to perform the second call? Or wait for the first one to finish? Or should we cancel the first one?
  2. What if we toggle different items in a short time?
  3. What if we want to introduce some sort of debounce or throttle?

If you know RxJS (and if you're reading this, you should by now!) you know that the first problem is easily solved with the 4 Flattening Operators (mergeMap, concatMap, switchMap, exhaustMap).

Then, if you know RxJS quite well, you know that you can solve the second problem with an awesome operator called groupBy!

But in order to use all of this goodness, you must have an Observable source, so... not a method.

Subjects

Think of a Subject like an open (not completed), empty Observable. It's the perfect tool to represent custom events.

All of the events in our component can be represented by Subjects:

export class TodoListComponent {
  ...

  toggleTodo$ = new Subject<string>();
  deleteTodo$ = new Subject<string>();
  addTodo$ = new Subject<void>();
}
Enter fullscreen mode Exit fullscreen mode

Then, our template can simply call them when necessary, instead of calling methods, for example:

<button (click)="deleteTodo$.next(todo.id)">delete</button>
Enter fullscreen mode Exit fullscreen mode

Now that our sources are Observables, we can use our dear operators: let's create some effects.

Effects

I like to define my effects inside the constructor so that I can use the takeUntilDestroyed() operator to clean up the effect when the component is destroyed! So, for example:

constructor() {
  this.addTodo$.pipe(
    concatMap(() => this.todoService.add())
    takeUntilDestroyed()
  ).subscribe(newTodo => this.todos.update(todos => [...todos, newTodo]));
}
Enter fullscreen mode Exit fullscreen mode

Here I'm using concatMap in order to preserve the order of the responses, so that the todos are added in order. This means that there are no concurrent calls. I think it's perfect for add operations, but it may be the wrong choice for other calls: for instance, for a GET request, it's usually better to use exhaustMap or switchMap, depending on the use case.

I'm also using an approach which is called Pessimistic Update, which means that I wait for the call to end to update my internal state. This is a personal preference! You could add the todo right away, and then revert it back by using a catchError if the API call errors out.

Then there's the actual effect function from Angular which is meant to be used in conjunction with signals: I use this function for synchronization tasks. For example, when a parameter changes in the URL (referring to a new entity ID), I may want to update a form with the new entity:

// This comes from the router
id = input.required<string>();

// Always stores the current invoice information
currentInvoice = toSignal(toObservable(this.id).pipe(
  switchMap(id => this.invoiceService.get(id))
));

constructor() {
  effect(() => {
    // Assuming the 2 structures match, every time we browse
    // to a new invoice, the form gets populated
    this.form.patchValue(this.currentInvoice());
  })
}
Enter fullscreen mode Exit fullscreen mode

Notice that we don't have control over back-pressure with this technique. For this kind of thing it's fine, but remember: that's why we still need RxJS in order to craft bug-free apps. Or another library which abstracts this complexity under the hood.

Going full Reactive is not always a good idea

Many states that we represent with signals could be technically be considered derived asynchronous states. For example, our Todo list could be considered a derived state from the server:

// Trigger this when you need to refetch the todos
fetchTodos$ = new Subject<void>();

todos = toSignal(toObservable(this.fetchTodos$).pipe(
  switchMap(id => this.todoService.getAll())
));
Enter fullscreen mode Exit fullscreen mode

This approach is similar to the one used by libraries such as TanStack Query, in which you manually invalidate a query when you need the new data. In other words, you always go to the server for each mutation.

This may be good in some scenarios, but there are 2 things to consider:

  1. It makes updating the state manually (optimistic updates) difficult. This is made easier by libraries such as TanStack Query, but doing it manually is a pain.
  2. It makes the code somewhat harder to grasp by most developers, this is what I see as a consultant working on this kind of stuff daily.

In short, I usually don't recommend it. And I said usually! :)

Conclusion

I hope you liked this short article! As a summary:

  • Define your states as signals
  • Define your derived states as computed signals
  • Define your asynchronous effect as Observables
  • Define your synchronozation effects with effects

I'm sure that if you follow these principles your apps will be much easier to maintain!

rxjs Article's
30 articles in total
Favicon
rxjs
Favicon
Angular Signals and Their Benefits
Favicon
Migrando subscribe Callbacks para subscribe arguments no RxJS
Favicon
New in Angular: Bridging RxJS and Signals with toSignal!
Favicon
RxSignals: The Most Powerful Synergy in the History of Angular 🚀
Favicon
Virtually Infinite Scrolling with Angular
Favicon
Building a Collapsible UI Component in Angular: From Concept to Implementation 🚀
Favicon
Disabling button on api calls in Angular
Favicon
Angular Dependency Injection — Inject service inside custom Rxjs operators
Favicon
Unsubscribe Observable! why is it so important?
Favicon
Harnessing the Power of RxJS with React for Asynchronous Operations
Favicon
DOM Observables, Rx7, Rx8 and the observable that doesn't exist yet
Favicon
Advanced RxJs Operators You Know But Not Well Enough pt 2.
Favicon
Observables in Node.js: Transforming Asynchronous Chaos into Elegant Code
Favicon
Reusable, Extensible and Testable State Logic with Reactive Programming.
Favicon
Understanding RxJS and Observables in Angular: A Beginner-Friendly Guide
Favicon
Are Angular Resolvers on Life Support ?
Favicon
Creating Custom rxResource API With Observables
Favicon
Forestry: The Best State System for React and (not React)
Favicon
📢 Announcing the New Reactive State Management Libraries! 🎉
Favicon
Reactables: Reactive State Management for any UI Framework.
Favicon
Hot vs Cold Observables in Angular: Understanding the Difference
Favicon
RxJS Simplified with Reactables
Favicon
How I structure my Angular components with Signals
Favicon
Angular LAB: let's create a Visibility Directive
Favicon
Difference between mergeMap vs switchMap vs concatMap vs exhaustMap
Favicon
How Signals Improve Your App Performance
Favicon
The Article I Took So Long to Write: Concepts about Reactive Programming and RxJS
Favicon
Tame Your Asynchronous World with RxJS
Favicon
Custom RxJS Operators to Improve your Angular Apps

Featured ones: