Angular Signals

By Syncfusion®, Inc. · Last updated

Storing and updating values with signals: writable signals, reading and updating values, computed signals, effects, signals compared with RxJS, and best practices.

What are Signals?

A signal is a box that stores one value. When the value in the box changes, Angular updates every part of the screen that reads it.

You create a signal with the signal() function, and you read its value by calling it like a function: count().

Real-World Example

On the cart page of an online store, the quantity of each item is stored in a signal. The subtotal, tax and total are calculated from the quantities.

When the user changes a quantity, Angular updates only the numbers that depend on it. The rest of the page stays as it is.

price = 50writable signalqty = 3writable signaltotal = 150computedTemplateshows Total: 150effect()writes a log lineChange qty and Angular recalculates total, then updates the template and runs the effect.

Signals pass a change to everything that reads them.

Why Signals?

In earlier lessons, normal fields updated the screen after a click. But a normal field does not update the screen when its value changes later, for example in a timer or after a server answer. A signal always updates the screen, whenever its value changes.

Benefits of signals:

  • The screen always shows the current value, after a click, a timer or a server answer.
  • Angular knows exactly what to update. It updates only the parts of the screen that read the changed signal.
  • Values can depend on other values. A computed signal calculates its value from other signals automatically.
  • Simple code. You read with count() and change with set() or update(). You do not need subscribe().
PointNormal fieldSignal
Createcount = 0count = signal(0)
Readcountcount()
Changecount = 5count.set(5)
Screen updates after a timer or server answerNoYes

Writable Signals

A writable signal is a signal whose value you can change. You create it with signal() and give it a starting value:

import { signal } from '@angular/core';

quantity = signal(1);           // a number
userName = signal('Guest');     // a string
items = signal<string[]>([]);   // an empty list of strings

For an empty list, write the type in < >, for example signal<string[]>([]). Otherwise TypeScript does not know what the list will contain.

Reading Signal Values

To read the value, call the signal like a function, with ():

  • In the template: {{ quantity() }}
  • In the class: this.quantity()

Creating and reading signals

import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <p>Product: {{ product() }}</p>
    <p>Price: {{ price() }}</p>
    <p>{{ stockMessage() }}</p>
  `,
})
export class App {
  product = signal('Wireless Mouse');
  price = signal(15);
  stock = signal(0);

  stockMessage() {
    return this.stock() > 0 ? 'In stock' : 'Out of stock';
  }
}

The method stockMessage() reads the stock signal with this.stock().

If you forget the (), you get the signal box, not its value. {{ price }} shows [Signal: 15] on the page, not 15.

Updating Signal Values

A writable signal has two methods to change its value:

set(value)update(fn)quantity = 3quantity.set(1)the old value is ignoredquantity = 1quantity = 3quantity.update(q => q + 1)uses the old valuequantity = 4

set() replaces the value. update() calculates it from the current value.

  • set(value) replaces the value with a new value. Example: quantity.set(1).
  • update(fn) calculates the new value from the current value. The function gets the current value and returns the new one. Example: quantity.update(q => q + 1).

Use set() when the new value does not depend on the old one. Use update() when it does.

You cannot change a signal with =. this.quantity = 5 is an error, because quantity is a signal, not a number. Always use set() or update().

set() and update()

import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <p>Quantity: {{ quantity() }}</p>
    <button (click)="decrease()">-</button>
    <button (click)="increase()">+</button>
    <button (click)="reset()">Reset</button>
  `,
})
export class App {
  quantity = signal(1);

  increase() {
    this.quantity.update(q => q + 1);
  }

  decrease() {
    this.quantity.update(q => (q > 1 ? q - 1 : 1));
  }

  reset() {
    this.quantity.set(1);
  }
}

Updating Lists and Objects

A signal reports a change only when it gets a new value. If you change the old list with push(), the signal still holds the same list, so it reports no change. Then a computed signal that reads the list, such as a count, keeps its old value.

Create a new list instead. [...list, item] copies the old items into a new list and adds the new item at the end.

Updating a list in a signal

import { Component, computed, signal } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <button (click)="add('Notebook')">Add notebook</button>
    <button (click)="add('Pen')">Add pen</button>
    <button (click)="clear()">Clear</button>
    <p>Items ({{ count() }}): {{ items().join(', ') }}</p>
  `,
})
export class App {
  items = signal<string[]>([]);
  count = computed(() => this.items().length);

  add(name: string) {
    this.items.update(list => [...list, name]);
  }

  clear() {
    this.items.set([]);
  }
}

count is a computed signal. You learn computed signals in the next section.

For an object, copy it in the same way: user.update(u => ({ ...u, name: 'Asha' })) creates a new object with a new name.

Computed Signals

A computed signal calculates its value from other signals. You create it with computed() and a function:

total = computed(() => this.price() * this.quantity());
  • Angular runs the function again only when price or quantity changes.
  • If they do not change, Angular reuses the last result. So reading total() many times is fast.
  • A computed signal is read-only. It has no set() or update().

A cart total with computed()

import { Component, computed, signal } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <p>Keyboard: $40 × {{ quantity() }}
      <button (click)="quantity.update(q => q + 1)">+1</button></p>
    <p>Subtotal: {{ subtotal() }}</p>
    <p>Tax (10%): {{ tax() }}</p>
    <p><b>Total: {{ total() }}</b></p>
  `,
})
export class App {
  price = signal(40);
  quantity = signal(1);

  subtotal = computed(() => this.price() * this.quantity());
  tax = computed(() => this.subtotal() * 0.1);
  total = computed(() => this.subtotal() + this.tax());
}

Click +1. subtotal changes because quantity changed. tax and total change because they read subtotal.

Effects

An effect is a function that Angular runs again each time a signal that it reads changes. You create it with effect().

theme.set("dark")a signal changesAngularfinds effects that read iteffect(() => ...)runs againSide effectlog, save, titleEffects are for work outside the screen. Values for the screen come from computed().

An effect runs again each time a signal it reads changes.

Use an effect for work outside the template, for example:

  • Writing a log message.
  • Saving a value in the browser's storage.
  • Changing the browser tab title.

Create the effect in the constructor. Like inject(), effect() works only while Angular creates the class.

An effect writes a log line

import { Component, effect, signal } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <button (click)="theme.set('light')">Light</button>
    <button (click)="theme.set('dark')">Dark</button>
    <p>Current theme: {{ theme() }}</p>
    @for (line of log(); track $index) {
      <p>{{ line }}</p>
    }
  `,
})
export class App {
  theme = signal('light');
  log = signal<string[]>([]);

  constructor() {
    effect(() => {
      const value = this.theme();
      this.log.update(list => [...list, `Theme saved: ${value}`]);
    });
  }
}

The effect runs once at the start, and again each time you click a different theme.

Do not use an effect to calculate a value from other signals. Use computed() for that. An effect is only for work outside the screen, such as logging or saving.

Signals vs RxJS

You learned RxJS Observables in the previous chapter. Signals and Observables solve different problems, and Angular apps use both.

Signal: one current valueObservable: values over timecount = 3read any time: count()123subscribe() receives each value when it happens

Signals hold state. Observables deliver events over time.

PointSignalObservable (RxJS)
HoldsOne current value, alwaysA series of values over time
Read the valuecount(), at any timesubscribe() or the async pipe
Has a value at the startYesNot always
Clean upNot neededUnsubscribe from Observables that never complete
Best forData shown on the screenEvents and server requests, with operators such as switchMap()

A simple rule: store data in signals, and use RxJS for events over time, such as a search box that sends requests.

Signals Best Practices

  • Call the signal with () to read its value, in both the template and the class.
  • Use computed() for values that come from other signals, for example a total from a price and a quantity. Do not copy the result into another writable signal.
  • Create new lists and objects in update(). Do not change them with push() or by setting a property.
  • Use effects only for work outside the screen, such as logging and saving.
  • Keep writable signals private in services. Give other classes a read-only version with asReadonly(), and change the value only with the service's methods:
export class CartService {
  private readonly _count = signal(0);
  readonly count = this._count.asReadonly(); // other classes can read, not set

  add() {
    this._count.update(n => n + 1);
  }
}

Common Mistakes

MistakeWhat you seeFix
{{ price }} without ()The page shows [Signal: 15], not 15Write {{ price() }}
items().push('Pen')Computed signals that read the list keep their old valueUse items.update(list => [...list, 'Pen'])
total.set(10) on a computed signalError: set is not a functionChange the signals that total reads
Calculating a value inside an effectExtra code, and values can be one step behindUse computed()
Calling effect() in a click methodError: effect() must be called from an injection contextCreate effects in the constructor

Check Yourself

Exercises

What to Remember

  • A signal stores one value. Create it with signal(), read it with (), and change it with set() or update(). For lists and objects, always create a new one.
  • computed() calculates a read-only value from other signals and recalculates only when they change.
  • effect() runs code outside the screen when signals change. Use signals for data on the screen, and RxJS for events over time.

Next: State Management, how to store data that many components share.