Skip to content

Lesson 4 — Signals and state

Objective

Build a signal store with writable source state, read-only derivation, and side effects only at true external boundaries.

Prerequisites

Component ownership, immutable TypeScript updates, and pure-function reasoning.

Mental model

A signal is a tracked current value. computed is lazy memoized derivation; an effect performs work outside pure derivation when dependencies change. If B is fully calculable from A, store A and derive B.

Why it exists

Signals make synchronous state reads and dependencies explicit. Derivation removes duplicate truth and synchronization failures.

Working model: reads register dependencies and writes notify consumers.

Engineering reality: Angular tracks reads in reactive consumers, invalidates cached computations, and schedules affected views; mutation that bypasses set/update does not create a notification.

Minimal example

readonly topics = signal<readonly Topic[]>([]);
readonly completed = computed(() => this.topics().filter(t => t.status === 'complete').length);

Real-world usage

Feature stores combine synchronous UI/domain state and derived facts. RxJS remains appropriate for temporal async pipelines; persistence is a genuine side effect.

Common mistakes

  • synchronizing derivable state with an effect;
  • exposing writable signals across arbitrary consumers;
  • mutating objects in place;
  • placing every feature store at root;
  • assuming signals replace error/completion/cancellation semantics.

Mini lab

Task: Create TrackerStore with topic/session/search source signals; computed filtered topics, counts, minutes, and completion; methods for status changes and sessions; inject it into feature components.

Constraints: immutable updates, no duplicated computed values, and no persistence inside domain transitions.

Expected result: filters do not mutate source topics, totals update predictably, and computed work is memoized until dependencies change.

Hint 1

List facts supplied from outside separately from facts entirely derivable from them.

Hint 2

Keep writable signals private or constrain writes through domain methods.

Solution direction — last resort

Use topics.update(topics => topics.map(...)); expose computed facts; reserve one persistence effect for an adapter boundary if added.

Verification

Log inside a computed: read twice, then change a dependency. Mutate an object without set/update and explain the stale consumer. Add an effect copying topics into a count, identify the synchronization risk, then replace it with computed.

Mastery evidence: classify source/derived/effect state, implement immutable transitions, and diagnose stale mutation unaided.

Bloom challenge

  1. Understand: signal, computed, and effect roles.
  2. Apply: derive completion from topics.
  3. Analyze: explain dependency invalidation and lazy recomputation.
  4. Evaluate: signals versus RxJS for a cancellable search.
  5. Create: design a scoped feature store with persistence and test seams.

Summary

Store irreducible source facts, derive the rest, and push effects to explicit external boundaries.

Spaced review

Lesson 5 tests store lifetime through provider scope. Lesson 9 asks you to defend the signal/observable bridge.

What comes next

Continue to dependency injection.