The Best Way to Use Signals in Angular Apps

Share on facebook
Share on google
Share on twitter
Share on linkedin

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.

Related Posts and Comments

Error, loading, content…? Use this page pattern for your Angular apps

When developing Angular applications, it’s common for pages to transition through three key states: error, loading, and show content. Every time you fetch data from an API, your page will likely show a loading indicator first, and then either render the content successfully or display an error message if something goes wrong. This pattern is

Read More »

How to Set up a CI pipeline with Azure Pipelines and Nx

It goes without saying that having a CI pipeline for your Angular apps is a must. Setting one up for regular Angular apps is fairly straightforward but when you have an Nx monorepo there are certain other challenges that you have to overcome to successfully orchestrate a “build once, deploy many” pipeline. This post will

Read More »