Since Angular 16, Angular now has experimental support for signals and there is a lot of confusion in the community about whether this is going to replace RxJS or how it should be used in an app in combination with RxJS.
This blog post sheds some light on what I think is the best way to use signals in your Angular apps.
What are signals?
Signals come from SolidJS and offer a reactive way of handling state. You can create computed
steams of signals that all get updated when one of the dependent signals changes.
Furthermore, signals are a big enabler for zone-less Angular apps as Angular will later support signal-based components, triggering change detection when the signals in a given component change. This is a significant optimization over Angular’s rather imprecise top-down change detection method (triggering change detection on each ZoneJS patched event).
Do Signals replace RxJS?
A common question is whether signals replace RxJS and they certainly can when it comes to synchronous reactivity, when it comes to asynchronous reactivity the RxJS operators can still be superior.
With Signals, you would keep your state in a Signal rather than having a behavior subject and you would get the values from the signal without subscribing or using async
pipe. It also allows us to react to changes so dependent signals are updated upon source changes. Signals don’t have all the operators RxJS has though such as debounceTime
which might make you implement this kind of operator logic yourself in your signals unless you convert your signal to RxJS and back again…
Core elements of signals
Signals core operators are computed
, update
, mutate
and set
.
Computed
The computed
the operator will compute a new value whenever one of the signals it uses changes. It’s similar to a selector in NgRx or a derived stream in RxJS. Furthermore, it accepts an optional second argument where you can provide a custom compare function to determine when to recompute the value. As default values are recomputed whenever there is a change to an object or whenever a primitive type changes value.
todoItems = computed(() => this.state().todoItems);
Update
The update
method is taking the existing state and returning the new state, kinda like a reducer function in NgRx.
this.state.update((state) => ({ ...state, todoItems: [...state.todoItems, newTodoItem], }));
Note, I recommend using this for state updates over mutate
and set
to ensure immutable state updates.
Mutate
The mutate
method is used for mutating the previous state and does not return any value.
this.state.mutate((state) => { state.todoItems = [...state.todoItems, newTodoItem] });
Set
The set
method will replace the whole state with whatever it is set with, it’s like next
on a BehaviorSubject
.
this.state.set({ todoItems: [...this.state().todoItems, newTodoItem] });
Building a todo app with Signals
Let’s consider how we can build a todo app with signals. In this app, we will use signals to contain all the state of the application and expose it through a service like a facade pattern. Alternatively, this could have been done with NgRx (store or component store) and it’s selectSignal
method.
Show todo items
We are getting some todo items from an imaginary endpoint and saving them in the signal that contains our state and use computed
to select the state.
We create the signals state like:
export interface TodoItem { id: string; name: string; isCompleted: boolean; } export interface TodoListState { todoItems: TodoItem[]; } @Injectable({ providedIn: 'root', }) export class TodoListService { state = signal<TodoListState>({ todoItems: [] });
And then we populate the state on init with this mock todo list in the TodoListService
:
fetchTodoItems() { this.state.update((state) => ({ ...state, todoItems: [ { id: '1', name: 'Create YT video', isCompleted: false, } as TodoItem, { id: '2', name: 'Go to the gym', isCompleted: false, } as TodoItem, { id: '3', name: 'Buy flowers', isCompleted: false, } as TodoItem, ], })); }
Not that when creating the “selectors” for this store, we will use computed
:
todoItems = computed(() => this.state().todoItems);
We can then use the service with these signals in the TodoListComponent
like:
export class TodoListComponent { todoItems: Signal<TodoItem[]>; constructor(private todoListService: TodoListService) { this.todoItems = todoListService.todoItems; } ...
And show them in the template as:
<app-todo-item class="mb-1" *ngFor="let todoItem of todoItems(); trackBy: todoItemsTrackBy" [todoItem]="todoItem" (delete)="onDeleteTodo($event)" (edit)="onEdit($event)" (isCompletedChange)="onIsCompletedChange($event)" > </app-todo-item>
Note how the value is unwrapped in the template by calling a method, which is normally a big no-no in Angular apps but in this case, it’s harmless as calling the method is a cheap getter function to get the value without any computation being triggered on template renderings.
Create todo items
Now we can show the todo items, let’s add a form that can create a new todo item in the signals store.
We create the form:
<form [formGroup]="formGroup" (submit)="onSaveTodo()"> <mat-form-field class="example-form-field" appearance="fill"> <mat-label>Todo name</mat-label> <input matInput type="text" formControlName="name" /> </mat-form-field> <button mat-button color="primary" [disabled]="formGroup.invalid" type="submit" > Save </button> </form>
On form submission, we will pass the form value to the TodoListService
which will handle the create logic:
saveTodo(todoItemToSave: TodoItem) { // create const newTodoItem = { ...todoItemToSave, id: crypto.randomUUID(), } as TodoItem; this.state.update((state) => ({ ...state, todoItems: [...state.todoItems, newTodoItem], })); }
And then, the new todo item is saved in the todo item.
Update todo items
We update the todo items by clicking the pen icon on a given todo which will populate the form with the todo item’s data.
The HTML for the todo item looks like this:
<mat-card class="pl-2"> <div class="flex justify-between items-center gap-4"> <div [class.line-through]="todoItem?.isCompleted"> {{ todoItem?.name }} </div> <div class="flex items-center"> <mat-checkbox color="primary" [formControl]="isCompletedFormControl"> </mat-checkbox> <button mat-icon-button (click)="onDelete()"> <mat-icon color="warn" fontIcon="delete"> </mat-icon> </button> <button mat-icon-button (click)="onEdit()"> <mat-icon color="accent" fontIcon="edit"> </mat-icon> </button> </div> </div> </mat-card
On clicking the pen the form is populated by setting the reactive form data:
onEdit(todoItem: TodoItem) { this.selectedTodo = todoItem; this.formGroup.setValue({ name: this.selectedTodo.name, }); }
And when the form is submitted, the same submit method as before is used but we have to extend the TodoListComponent
‘s saveTodoItem
method to handle update as well. This is done by checking if a todo item already has an id (update) and then finding and updating the todo item with that same id
:
saveTodo(todoItemToSave: TodoItem) { if (todoItemToSave.id) { // update const updatedTodoList = this.state().todoItems.map((todoItem) => { if (todoItem.id === todoItemToSave.id) { return todoItemToSave; } return todoItem; }); this.state.update((state) => ({ ...state, todoItems: [...updatedTodoList], })); } else { // create const newTodoItem = { ...todoItemToSave, id: crypto.randomUUID(), } as TodoItem; this.state.update((state) => ({ ...state, todoItems: [...state.todoItems, newTodoItem], })); } }
Delete todo items
Lastly, we can delete a todo item by clicking the trash bin icon on the todo item.
Upon clicking we propagate the id of the todo item to the TodoListService
to delete the todo item. Here we will get the todo item from the signals store and remove the one with the given id:
deleteTodo(todoItemId: string) { const newTodoList = this.state().todoItems.filter( (todo) => todo.id !== todoItemId ); this.state.update((state) => ({ ...state, todoItems: newTodoList })); }
Demo project
The demo project can be found on my Github here.
Should you use Signals already?
Signals can simplify your synchronous reactivity compared to RxJS but RxJS is still the tool for asynchronous reactivity eg. handling the data response from a backend request. Also, RxJS has more powerful operators that might simplify certain scenarios such as implementing debounce time on a search field.
The big game-changer would be when Signal-based components are ready as this will allow you to run zoneless as explained in this rfc.
Conclusion
Signals are a big step in Angular for a more simple developer experience while supporting better performance with zone-less apps once Signal-based components are ready.
If you want to learn more about Angular architecture with Signals I recommend you check out the next cohort of my training here.
Do you want to become an Angular architect? Check out Angular Architect Accelerator.