Course outline

Angular / Basic / Lesson 3

Signals and derived state

Model writable state and pure derivations with signals while keeping mutation, equality, and effect lifetimes explicit.

About 5 min readContent reviewed
On this page

Lesson goals

  • Read, set, and update writable signals.
  • Use computed for state derivation instead of synchronization effects.
  • Explain reference equality and template signal reads.

A signal is a tracked value accessed through a getter

signal creates a writable reactive value. Calling its getter reads the current value; set replaces it and update calculates a replacement from the previous value. The signal wrapper does not make an arbitrary object deeply immutable. A computed signal derives a value from other signal reads and is read-only through its public interface. Angular tracks the dependencies actually read during a computation, so a conditional derivation can change its dependency set. Keep those derivations pure and understandable. Signals provide a state relationship; they do not automatically validate external data, authorize changes, or persist values across application restarts.

Verify a derivation with the real signal implementation

The complete program imports the pinned Angular core package and uses no component, browser, or injection context. computed is lazy and memoized, so its value is derived when read and can be reused until relevant dependencies change. Here the numeric values are small and the relationship is simply double the count. After update changes the source from two to five, the next read derives ten. This confirms the isolated signal computation, not DOM scheduling or component change detection. Keep that limitation explicit when reporting a local test. Framework rendering needs its own fixture.

typescript Complete Angular-core signal computation
import { signal, computed } from "@angular/core";
const count = signal(2);
const doubled = computed(() => count() * 2);
console.log(doubled());
count.update(value => value + 3);
console.log(doubled());
Expected output
4
10

The reads print four and ten. Only the local signal graph is exercised; no application or browser is bootstrapped.

Read the getter when rendering a signal

The contextual component reads count() and doubled() in its template. Reading the wrapper name without calling it refers to the signal function rather than its current value. Angular tracks signal reads used by a template, including an OnPush component, so relevant changes can mark that view for update. The click handler performs one source update; the derived value does not require another writable field. This fragment needs a bootstrapped Angular host, so expected visible values are described rather than claimed browser results. It deliberately avoids effects whose execution also requires a managed context and lifecycle.

typescript Contextual signal reads and one source update
import { Component, computed, signal } from "@angular/core";
@Component({
  selector: "app-signal-counter", standalone: true,
  template: `<button type="button" (click)="count.update(increment)">Increase</button>
             <p>{{ count() }} / {{ doubled() }}</p>`
})
export class SignalCounter {
  readonly count = signal(2);
  readonly doubled = computed(() => this.count() * 2);
  readonly increment = (value: number) => value + 1;
}

The initial view would show 2 / 4, then 3 / 6 after one click in a valid component fixture. These DOM observations are not produced by the local signal program.

Replace changed objects instead of silently mutating them

Default signal equality uses reference-aware comparison, so changing an array in place and setting the same reference may not notify consumers as intended. Return a new array or object from update when its state changes. A computed derivation is usually better than copying derived values through an effect. Effects suit integration with imperative systems, not an automatic substitute for every data relationship. Their creation and cleanup have injection-context and lifecycle requirements that later lessons cover. Custom equality can suppress updates too, so use it only with a tested equivalence rule that matches what consumers observe.

Test source state, derivation, and view independently

Test initial reads, updates, conditional dependencies, and array replacement with small synthetic values. A signal test can establish derived state without proving keyboard behavior, asynchronous rendering, or persistence. A component test should additionally verify the text after the interaction and the relevant render synchronization. Avoid effects that write back into their own dependencies and create loops or difficult ordering. State updates should preserve domain invariants, and server mutations still need server-side validation. A clear signal graph gives each fact one authoritative source and makes derivations reviewable instead of scattering synchronized copies across fields.

Common errors and fixes

The template displays a signal function rather than its value.

Why it happens
The getter was not called.
How to fix it
Read count() in the template.
Prevent it next time
Distinguish the wrapper from its current value.

An in-place array update does not refresh a derived view.

Why it happens
The update retained a reference considered equal.
How to fix it
Replace the value immutably through set or update.
Prevent it next time
Test object identity alongside expected state.

An effect is used to keep two derived fields synchronized.

Why it happens
A pure dependency was modeled as imperative copying.
How to fix it
Use computed for the derivation.
Prevent it next time
Give each state fact one authoritative source.

Check your understanding

1. Extend the local program with count.set(0). State both source and derived values and explain why zero needs no separate effect.

Show worked solution

The source reads zero and doubled reads zero. computed already expresses the relationship count times two, so another effect or writable doubled field would duplicate that rule and risk drift. Add an assertion for the new read in the local model and a separate rendered-text check if the user-facing component must show the result.

2. A signal holds [1, 2] and a button should append 3. Describe a replacement update and the invariant a test should check.

Show worked solution

Use an update returning [...values, 3] rather than mutating values with push and returning the same array. Verify the resulting sequence is [1, 2, 3] and that the new array is a different reference from the previous value. Also test any domain rule such as duplicate prevention independently; immutable replacement does not enforce business validity by itself.

Key takeaways

  • Read signals through their getters.
  • Derive with computed and replace changed objects.
  • Separate local reactivity evidence from rendering evidence.

Official references