skip to content

In Angular, how do you create a writable signal with signal(), read its value, and change it with set() versus update()?

level: juniorimportance: must knowfreq 78%

answer

  1. a getter function, not a property
  2. call it to read, even in templates
  3. replace outright or derive from previous
  4. WritableSignal, and a read-only view of it

basics

~20 s

signal(initialValue) returns a WritableSignal: a getter function you call, such as qty(), to read the value. set(value) replaces the value outright, while update(fn) computes the next value from the current one and must return it.

solid answer

~40 s

`signal(initial)` from `@angular/core` returns a `WritableSignal<T>`. It is a zero-argument getter, so you read it by calling it, `qty()`, in class code and in templates alike; `{{ qty }}` without the call is a bug the compiler's `NG8109` extended diagnostic warns about. `set(value)` replaces the value; `update(fn)` passes the current value to `fn` and stores whatever `fn` returns, so it is the natural choice for increments and immutable edits. Both apply synchronously and notify dependents only if the new value differs under the signal's equality check (`Object.is` by default). To hand out state without handing out write access, expose `asReadonly()`, which returns a plain `Signal<T>`. The old `mutate()` method was removed in v17, so in-place edits are not an API any more.

code

ts · 26 lines
ts
import {Component, signal} from '@angular/core';

@Component({
  selector: 'app-qty-stepper',
  template: `
    <button (click)="dec()" [disabled]="qty() <= 1">-</button>
    <span>{{ qty() }}</span>
    <button (click)="inc()">+</button>
    <button (click)="reset()">Reset</button>
  `,
})
export class QtyStepper {
  readonly qty = signal(1);

  inc() {
    this.qty.update(q => q + 1);
  }

  dec() {
    this.qty.update(q => Math.max(1, q - 1));
  }

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

go deeper

for a junior

Recall that signal() returns a getter you call to read, and that set() replaces while update() derives the next value from the current one. Show both in a small component.

for a middle

Explain that both writers run the equality check first, store synchronously, and notify only on a real change, and why update() must return a new reference for arrays and objects.

for a senior

Show how you structure services around private writable signals with asReadonly() views, and how you keep writes out of templates and computed() to avoid NG0600.

for a principal

Frame the tradeoff of making signals the team's default state primitive: simpler reads and writes versus the discipline immutable updates and read-only exposure require across a codebase.

## What `signal()` returns In Angular, a **signal** is a wrapper around a value that notifies interested consumers when that value changes. You create a writable one with `signal()` from `@angular/core`: ```ts import {signal, WritableSignal} from '@angular/core'; const qty: WritableSignal<number> = signal(1); ``` The returned object is a **function** with extra methods attached. That shape is the whole API: - calling it, `qty()`, **reads** the current value; - `qty.set(value)` **replaces** the value; - `qty.update(fn)` **derives** the next value from the current one; - `qty.asReadonly()` returns a **read-only** `Signal<number>` view of the same state. Signals arrived in v16 as a developer preview and have been stable public API since v17; in Angular 22.2 they are the default way to hold component and service state. ## Reading is a call, everywhere Because a signal is a getter, reading it always means calling it. That is also how Angular knows *who* read it: when the call happens inside a reactive context such as a template, a `computed()` or an `effect()`, the reader is recorded as a dependency. - In class code: `const n = this.qty();` - In a template: `{{ qty() }}` and `@if (qty() > 0) { ... }` Writing `{{ qty }}` without parentheses interpolates the function itself instead of its value. The compiler's extended diagnostic **`NG8109` (`interpolatedSignalNotInvoked`)** exists precisely to catch that slip. ## `set()` versus `update()` | Method | Argument | Use it when | Example | |---|---|---|---| | `set(value)` | the new value | the new value does not depend on the old one | `qty.set(1)` on reset | | `update(fn)` | `(current) => next` | the new value is computed from the current one | `qty.update(q => q + 1)` | | `asReadonly()` | none | you want callers to read but not write | `readonly qty = this._qty.asReadonly()` | Both writers behave the same way once the next value is known: 1. Angular compares the old and new value with the signal's **equality function**, `Object.is` unless you passed an `equal` option. 2. If they are equal, nothing happens: the stored value is kept and no dependent is notified. 3. If they differ, the value is stored **immediately** and dependents are notified, so the very next `qty()` returns the new value. `update()` is not a mutation API. It stores the **return value** of your function, so an arrow body with braces and no `return` stores `undefined`. For objects and arrays the function must return a **new** reference, for example `items.update(list => [...list, item])`. Angular's `WritableSignal` once had a `mutate()` method for in-place edits; it was removed in v17, and the migration note says to use `update()` with an immutable change instead. ## Exposing state read-only with `asReadonly()` A common service shape keeps the writable signal private and publishes a read-only view: ```ts @Injectable({providedIn: 'root'}) export class CartState { private readonly _qty = signal(0); readonly qty = this._qty.asReadonly(); add() { this._qty.update(q => q + 1); } } ``` The read-only signal reflects every change to the original but has no `set()` or `update()`. Two limits are worth knowing: - It does **not** freeze or deep-protect the value. A caller can still mutate a nested object it reads, which bypasses notification entirely. - It is a type-level and API-level boundary, not a security boundary: anyone holding the private field can still write. ## Where writes are not allowed Writes are fine in event handlers, lifecycle hooks, output handlers and effects. Writing to a signal while a `computed()` is computing or while a template is being evaluated throws **`NG0600`** ("Signal write in a disallowed context"). The fix is to move the write to where the change actually happens, or to express the value as a `computed()`. ## Common mistakes interviewers probe - Reading a signal like a property (`qty` instead of `qty()`), in class code or a template. - Using `update()` to push into an array and returning the same array, which the default equality treats as unchanged. - Assuming `set()` queues the change until change detection; the value is stored synchronously. - Assuming `asReadonly()` makes nested objects immutable.

  • In Angular, what happens when you call set() with the value the signal already holds?
    Nothing observable. `set()` compares the old and new value with the signal's equality function, `Object.is` by default; if they are equal the stored value is kept and no dependent is marked, so no `computed()` recomputes and no view is refreshed on its account.
  • How do you stop Angular components from writing to a signal that a service owns?
    Keep the `WritableSignal` private in the service and expose `asReadonly()`, which returns a `Signal<T>` with no `set()` or `update()`. Changes go through service methods. It does not deep-freeze the value, so objects inside it should still be treated as immutable.
  • Why did Angular remove WritableSignal.mutate() in v17?
    It edited the value in place and forced a notification, which made signals holding objects behave differently from every other write. v17 dropped it from the public API; the recommended replacement is `update()` returning a new object or array, so the default reference equality sees the change.

A signal is like a shared whiteboard with a sign-up sheet: anyone who reads it signs the sheet, set() wipes and rewrites it, update() rewrites it based on what is already there, and only the people on the sheet are told when the text really changes.

saying these in an interview costs you the question

  • A signal's value is read as a property, qty, without calling it.
  • update() edits the current value in place and needs no return value.
  • set() and update() queue the new value until the next change detection pass.
  • asReadonly() deep-freezes the value so nested objects cannot be changed.
  • Every set() notifies dependents, even when the new value is identical.