Angular Signals Course
Foundations Beginner Module 1 · ~25 min · Angular 21.2

What is a Signal?

Reading, writing, and the mental model of reactive values.

By the end you can

  • Explain what a signal is and how it differs from a plain variable
  • Create writable signals and read them in code and templates
  • Update state correctly with set() and update()
  • Understand why signals need to be called as functions to be read
  • Distinguish read-only signal APIs from deep immutability

APIs introduced

  • signal()
  • .set()
  • .update()
  • .asReadonly()

The one-sentence mental model

A signal holds a value and can notify reactive consumers when that value changes. Read it by calling its getter function; update a writable signal through its write methods.

A reactive consumer can be a template, a computed(), or an effect(). Angular tracks signal reads while that consumer executes. An ordinary call such as console.log(count()) reads the value without creating a subscription. Signals help Angular target view checking and work in both zone-based and zoneless apps.

Creating and reading a signal

You create one with the signal() function, giving it an initial value. You read it by calling it like a function.

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

// Create a writable signal with an initial value.
const count = signal(0);

// Read it by CALLING it — note the parentheses.
console.log(count()); // 0

// It's just a value container that tracks its readers.
const name = signal('Ada');
console.log(name()); // 'Ada'

A writable counter

Live playground

Every button calls set() or update(). Watch the read-out track the value instantly.

count()0

Writing: set() vs update()

There are exactly two ways to change a writable signal:

  • •set(value) — replace the value outright. Use it when the new value doesn't depend on the old one.
  • •update(fn) — compute the next value from the current one. Use it for increments, toggles, and appends.
ts
const count = signal(0);

// set(): replace the value outright.
count.set(10);        // count() === 10

// update(): derive the next value from the current one.
count.update(n => n + 1);   // 11
count.update(n => n * 2);   // 22

// Toggling a boolean is the classic update() case.
const open = signal(false);
open.update(v => !v); // true

Reading in a template

In a template you call the signal. Angular tracks the consuming view and can schedule it for checking when the signal changes. Other bindings in that view can also be evaluated; this is not an expression-by-expression rendering guarantee.

html
@Component({
  template: `
    <h1>Hello, {{ name() }}</h1>
    <input [value]="name()" (input)="name.set($any($event.target).value)" />
  `,
})
export class Greeting {
  name = signal('Ada');
}
// Changing name marks its consuming view for checking, including other bindings.

Live template read

Live playground

Type below. The heading re-renders on every keystroke because it reads name().

Hello, Ada Lovelace 👋

Signals of objects & arrays

A signal can hold objects and arrays. Prefer immutable updates: create a new value rather than modifying the existing one. By default, writes compare values with Object.is. Mutating a nested property bypasses the signal write API and does not notify consumers. An unrelated view check may reveal the mutation while a cached computed remains stale.

ts
const profile = signal({ first: 'Grace', last: 'Hopper' });

// ✅ Replace with a new object (spread the old, override a field).
profile.update(p => ({ ...p, first: 'Ada' }));

// ❌ Mutating in place does not notify consumers; cached derivations can go stale.
profile().first = 'Ada'; // don't do this

// Arrays follow the same rule:
const todos = signal<string[]>([]);
todos.update(list => [...list, 'Learn signals']); // ✅ new array

Immutable object updates

Live playground

Each edit spreads the old object into a brand-new one. The original is never touched.

profile(){"first":"Grace","last":"Hopper"}

Protecting state with read-only signals

A WritableSignal exposes set() and update(). Keep it private and expose .asReadonly() to remove those methods from the public API. This does not freeze contained objects: use readonly types and immutable updates to protect their shape too.

ts
class ScoreService {
  // Writable, but kept private to this class.
  private _score = signal(0);

  // Public, read-only view. Callers can read() but not set()/update().
  readonly score = this._score.asReadonly();

  award(points: number) {
    this._score.update(s => s + points);
  }
}
// service.score()      ✅ read
// service.score.set(9) ❌ compile error — no set() on a read-only signal

Read-only score

Live playground

The template can only read score(). Points can only be awarded through the component's own methods.

score()0

🎯 Your turn: hit exactly 10

Here's a signal starting at 3. Using only the buttons, land it on exactly 10. The badge below is driven by a computed() (a preview of Module 2) that watches the value — no manual checking. Think about which button uses update() and how the derived state reacts.

Reach 10

Live playground

Goal: target() === 10. Combine the steps.

target()3
Keep going — currently 3.

Key takeaways

  • ✓A signal notifies consumers that tracked its value in a reactive context.
  • ✓Create with signal(initial); read by calling it: count().
  • ✓set() replaces; update() derives from the current value.
  • ✓For objects/arrays, replace the reference immutably — never mutate in place.
  • ✓Expose .asReadonly() to hide the write methods; contained objects are not frozen.
Reach the end or solve the challenge to mark this module complete.