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/updatedoes 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¶
- Understand:
signal,computed, andeffectroles. - Apply: derive completion from topics.
- Analyze: explain dependency invalidation and lazy recomputation.
- Evaluate: signals versus RxJS for a cancellable search.
- 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.