Head Start with Angular Signals: A Basic Overview

Dany ParedesDany Paredes
7 min read

Since Angular 16, I have heard a lot about Signals, the @angular/team did great work listening to the community and improving the Signals API with Rxjs interop.

Yesterday I was talking with my friend Juan Berzosa he hadn't played with Signals yet, So I decided to write a small article that explained what are signals and how they help angular to have a better reactive and change detection system.

Before diving into Signals, let's ask ourselves why the following code doesn't respond to changes ?

We have the CardsService , with three properties cards, cardsLeft, lastClient and the add method. The method add, push a new value into the cards array, decreases the value of cardsLeft and set the holder name.

@Injectable({ providedIn: 'root' })
export class CardsService {
  cards: Array<Card> = [
    {
      id: '1',
      holder: 'Dany',
      status: 'Active',
    },
    {
      id: '2',
      holder: 'Edgar',
      status: 'Active',
    },
  ];

  cardsleft: number = 3;
  lastClient = 'No clients yet!';

  add(holder: string) {
    const card = {
      id: Math.random().toFixed(),
      holder,
      status: 'pending',
    };
    this.cards = [...this.cards, card];
    this.cardsleft--;
    this.lastClient = `Thanks ${holder}!!`;
    console.log(this.cards, this.lastClient, this.cardsleft);
  }
}

In the component, we use cardService properties to display information in the template, showing the cards list from cardService and the values from cardLeft and lastClient.

The save button calls the add method from the service, which pushes and changes the array and properties, but why isn't it reflected in my component?


@Component({
  selector: 'my-app',
  standalone: true,
  imports: [CommonModule],
  template: `
   <h3>We have {{cardLeft}} cards </h3>
   <p>{{lastClient}}</p>
   <input type="text" #holder />
   <button (click)="add(holder)">Save</button>
   <div *ngFor="let card of cards">
   {{card.holder}} {{card.id}}
   </div>
  `,
})
export class App {
  private cardsService = inject(CardsService);

  cards = this.cardsService.cards;
  cardLeft = this.cardsService.cardsleft;
  lastClient = this.cardsService.lastClient;

  add(holder: HTMLInputElement) {
    this.cardsService.add(holder.value);
    holder.value = '';
  }
}

Feel free to play:

The service modifies the properties, but those changes are not reflected in my component. These properties don't notify Angular that they have changed. Some solutions include:

  • Reasign the values.

  • Use a reactive approach with Rxjs.

What if I told you we can write a real reactive code and it reacts to changes, easily without Rxjs or reassigning values?

Reactivity, Change Detection, and NgZone

Do you know how Angular detects changes? Angular listens to events or data changes in the app by using Change detection in detection across the entire application using NgZone.

The NgZone makes change detection easier in Angular apps, but when the app gets larger, ZoneJS can slow it down and impact performance. In fact, we take some approaches to address this issue:

  • Using OnPush change detection

  • Using running code outside Angular with ngZone.runOutsideAngular(() => ...), to make change detection happen less often and speed things up.

However, another side effect is that ngZone doesn't indicate which component changed, only that something occurred in the app. Angular must check all components to identify the one that changed.

To learn more about Change Detection, watch video by Decoded Frontend.

The Angular team reevaluated how reactivity works to provide a building block and an excellent solution, keeping the following points in mind:

  • Know about which and when the application state changes.

  • Allow developers to indicate where it is used and when it changes.

  • Let Angular automatically update whatever depends on that value.

  • Gain fine-grained control of updates.

Taking those points into consideration, the Angular team introduced Angular Signals.

It's ready for use in developer preview starting from Angular version 16.

Signals

Signals enable fine-grained reactivity into Angular's core, providing a new way to recognize when our data has changed. They enhance Angular's change detection by running it only for the component change the data, which improves the efficiency of your Angular application and makes our code more reactive.

Ok, but what is a Signal, signal is a value accompanied by change notifications, which offers updates to its references and can be utilized in components for reading values, services, or templates and everywhere.

Signals offer three primary types, known as reactive primitives. These building blocks are provided by Angular, enabling us to work with Signals effectively. The three main types are: Writable Signals, Computed Signals, and Effects.

Writable Signals

We declare a signal variable using signals() function, and set an initial value. It holds a variable and notifies angular when it occurs.

quantity = signal<number>(3000)

To read the value of the 'quantity' signal, we use parentheses () to access the value.

payment() //3000

When we create a variable using the 'signal' function, it becomes writable and provides methods for working with it, such as 'set', 'update', or 'mutate'.

Set:

The set method replaces the value of a signal with a new value. When the signal is updated, the code notifies consumers that the signal has changed.

   payment.set(3000);
   payment() // 3000

Update:

The update() method changes the signal using its present value. Give the update method a simple function. This function offers the current signal value for you to change as required.

typescript payment.update(p => p + 100) payment(); //3100

mutate:

Thanks to Rainer Hahnekamp , I got the notice Angular Team has marked Signals as stable for version 17 and removed the mutate method from public API.

My secret trick to stay updated on what's happening in the Angular world is ngnews in YouTube and Twitter (x).

https://github.com/angular/angular/commit/c7ff9dff2c14aba70e92b9e216a2d4d97d6ef71e

Image

The mutate() method modifies the content of a signal value, not the signal value itself. Use it with arrays to modify array elements, and objects to modify object properties.

The consumers are not notified about the signal being changed, it must read the new signal value.

demo = signal<any>({ id: 1 }); 
demo.mutate(p => { id: 2 })

Computed Signals:

A computed signal is a variable that reflects changes between signals and read-dependent signals, and only changes if the values it depends on change, they are only calculated once even if used in three different places.

Computed Signals are read-only, they cannot be modified with 'set', 'update', or 'mutate'.

soldOut = computed(() => this.cardSlots() <= 0);

Effect

Effects are functions that execute when the values of the signals they depend on change. We can tell Angular to run a function that uses values from signals, and Angular will automatically rerun the code-associated signals change.

 effect(() => {
      if (this.soldOut()) {
        this.title = 'Bank Closed';
      }
  });

Refactor With Signals

We already know what a signal is, as well as the three types: WritableSignal, Computed, and effect. With this knowledge, we can refactor our code.

First, we turn the cards array into a Signal cards array, cardSlot and lastClient in a writable signal.

  cards = signal<Card[]>([
    {
      id: '1',
      holder: 'Dany',
      status: 'Active',
    },
    {
      id: '2',
      holder: 'Edgar',
      status: 'Active',
    },
  ]);


  cardSlots = signal<number>(3);
  lastClient = signal('No clients yet!');

Next, declare a computed to react to cardSlots changes.

 soldOut = computed(() => this.cardSlots() <= 0);

In the add method updates the signals props with set and update methods.

  add(holder: string) {
    const card = {
      id: Math.random().toFixed(),
      holder,
      status: 'pending',
    };
    this.cards.update((p) => [...p, card]);
    this.cardSlots.update((p) => p - 1);
    this.lastClient.set(`Thanks ${holder}`!!);
  }

In the component, add a reference to those variables.

 cards = this.cardsService.cards;
  cardSlots = this.cardsService.cardSlots;
  soldOut = this.cardsService.soldOut;
  lastClient = this.cardsService.lastClient;

In the HTML markup, access the signal value by using parentheses ().

    <h1>{{title}}</h1>
    <h3>We have {{cardSlots()}} slots </h3>
    <p>{{lastClient()}}</p>
    <input type="text" #holder [disabled]="soldOut()"/>
    <button (click)="add(holder)"
     [disabled]="soldOut()">Save</button>

    <div *ngFor="let card of cards()">
    {{card.holder}} {{card.id}}
    </div>

Finally, utilize the effect in the constructor, reading the 'soldOut' signal to update the title:

  constructor() {
    effect(() => {
      if (this.soldOut()) {
        this.title = 'Bank Closed';
      }
    });
  }

The final code looks like:

Conclusion

Done! We have a basic overview of how to make your apps reactive using Signals. Signals are game-changers in Angular bringing fine-grained reactivity, efficiency, and performance improvements to our apps.

But what about RxJS? How do Signals impact it? I will discuss this in another article. Meanwhile, I recommend checking out the official documentation to learn more about it.

Happy coding!

10
Subscribe to my newsletter

Read articles from Dany Paredes directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Dany Paredes
Dany Paredes

I'm passionate about front-end development, specializing in building UI libraries and working with technologies like Angular, NgRx, Accessibility, and micro-frontends. In my free time, I enjoy writing content for the Google Dev Library, This Is Angular Community, Kendo UI, and sharing insights here.