Advanced
Signals
Master Angular Signals, covering reactive values, computed dependencies, side-effects, and fine-grained change detection.
1. Learning Objectives
In this lesson, you will master Angular Signals, the modern reactive engine of Angular. By the end of this topic, you will be able to:
- Explain what a Signal is and how it differs from raw variables and RxJS Observables.
- Create and modify state using writable signals (
signal,set,update). - Define read-only derived state using lazy, memoized signals (
computed). - Register reactive side effects (
effect) and manage execution contexts. - Describe how Signals enable fine-grained change detection and Zoneless Angular.
2. Overview
A Signal is a wrapper around a value that can notify interested consumers when that value changes. Signals are reactive primitives: they track where they are read at runtime, allowing Angular to establish a dependency graph of your application's state. When a signal changes, Angular knows exactly which components and DOM elements need to be updated, enabling fine-grained rendering without checking the entire component tree.
3. Why This Topic Matters
Signals represent the future of Angular performance and developer experience:
- Zone.js Elimination: Traditionally, Angular relies on Zone.js to monkey-patch all asynchronous browser APIs to detect when *anything* might have changed, causing massive global digest cycles. Signals let Angular know exactly *what* changed, allowing applications to run without Zone.js (Zoneless).
- Automatic Dependency Tracking: Unlike RxJS, where you must manually chain operators, coordinate subscriptions, and combine streams (using
combineLatest), computed signals dynamically track whatever signals are read inside their body, updating automatically.
4. Real-World Analogy
Think of Signals like an **Interactive Spreadsheet (like Excel)**:
- Writable Signal (Cell A1): You manually type a number into cell A1, say
10. This is a source cell. - Computed Signal (Cell B1): You write a formula in cell B1:
=A1 * 2. Excel dynamically tracks that B1 depends on A1. You don't write a script to listen to changes on A1; Excel updates B1 automatically when A1 changes. If cell A1 doesn't change, Excel returns the cached value in B1 without recalculating. - Effect (Cell printout/render): A printer connected to the sheet automatically prints the updated sheet whenever cells compute new totals.
5. Core Concepts
Angular Signals are divided into three main primitives:
| Primitive | Writable? | Characteristics | Best Use Case |
|---|---|---|---|
| signal(val) | Yes | Updated using `.set()` or `.update()`. | Primary source state (e.g. quantity, items, user). |
| computed(fn) | No | Lazy evaluation, memoized (cached results), auto dependency-tracked. | Derived states (e.g. total, item count, filtered list). |
| effect(fn) | No | Runs asynchronously inside injection context when read signals change. | Side-effects (e.g. analytics logging, localstorage sync). |
6. Syntax & API Reference
This is the basic syntax for working with Signals, Computed values, and Effects:
7. Visual Diagram
This dependency graph shows how signal writes update computed derivations and trigger effects:
8. Live Example — Full Working Code
Here is a complete standalone Angular component implementing a reactive shopping invoice calculator using signals:
What just happened? Clicking "+10" calls incrementPrice(), which updates the price signal. Because subtotal depends on price, it is flagged as stale. When Angular renders the view, it reads subtotal, triggering recalculation of subtotal, tax, and total. Finally, the constructor's effect() detects the modification of total and logs the result to the developer console.
9. Interactive Playground
Try It Yourself Challenges:
- Add a
discountwritable signal to the Invoice Calculator. Update thesubtotalcomputed signal to subtract the discount from the total price. - Modify the
effect()block to store the current price inlocalStoragewhenever it changes, then read this cached price as the initial value when initializing the signal.
10. Common Mistakes
| Mistake | Why it happens | Wrong | Correct |
|---|---|---|---|
| Mutating Signal Objects Directly | Mutating an array or object property directly without calling .set() or .update() with a new reference prevents observers from detecting changes. |
this.todos().push(item) |
this.todos.update(t => [...t, item]) |
| Writing to Signals inside Computed block | Computed blocks are meant to be pure functions. Writing to a signal inside them creates loop cycles and throws compilation errors. | computed(() => { |
computed(() => val() * 2) |
11. Best Practices
- Keep computed functions pure: Never perform side effects or mutate state inside a
computed()signal; it should only transform read inputs. - Always emit new references: Keep state immutable by using the ES6 spread operator when updating arrays or objects inside
.update(). - Expose readonly signals from services: Use
asReadonly()to restrict write capabilities to your services:public items = this.itemsSubject.asReadonly(). - Use effects sparingly: Only use
effect()for logging, syncing with external browser storage, or handling custom Canvas/Charts canvas redraws.
12. Browser Compatibility/Requirements
Angular Signals are built on vanilla ES2022 features and perform consistently in all modern web browsers. They require Angular 16+ or newer.
13. Interview Questions
Q1: How do Signals differ from RxJS Observables?
Answer: Signals are synchronous state containers that always hold a value, are lazy/cached (memoized), and automatically track their dependencies dynamically at runtime. Observables are asynchronous event streams that do not necessarily have an active state value, require manual subscription cleanup, and require explicit combination operators (like combineLatest) to track dependencies.
Q2: Why are computed signals called lazy and memoized?
Answer: Computed signals are **lazy** because their derivation logic doesn't execute until something reads their value. They are **memoized** because they cache their computed value. If they are read multiple times while their dependent signals haven't changed, they return the cached value instantly without running the derivation function again.
14. Debugging Exercise
Identify why the console statement inside this effect prints twice immediately upon mounting, and how to prevent it:
Diagnosis: The effect executes once initially when created (printing "Guest - 0"). When ngOnInit fires, this.name.set('Admin') updates the signal. The effect detects this dependency change and schedules another execution, printing "Admin - 0".
Fix: If you want to read a signal inside an effect without tracking it as a dependency (so changes to it don't trigger re-execution), wrap the read in the untracked() utility.
15. Practice Exercises
Exercise 1: Build a reactive search filter store
Create a component with a search query signal searchQuery = signal('') and a list of users signal. Build a computed signal filteredUsers that automatically returns user matches containing the query string (case-insensitive).
16. Scenario-Based Challenge
The Dynamic Dashboard Grid Layout:
You are designing a modular dashboard. Users can add widgets, resize columns, and drag panels. The grid sizing computations are expensive. Build a state system utilizing Angular Signals that stores widget locations and sizes, computing final CSS pixel coordinates lazily and caching layout calculations to ensure drag-and-drop actions perform at 60 FPS.
17. Quick Quiz
Q1: Which method should you use to modify a signal's value based on its previous state?
A) set()
B) update()
C) mutate()
Answer: B — update() takes a callback function receiving the previous state and returning the next value.
18. Summary & Key Takeaways
- Signals are synchronous reactive wrappers that notify consumers of updates.
- Computed signals calculate derived state lazily and cache their values.
- Effects run side-effects when their read dependencies update, and require an injection context.
- Signals enable fine-grained reactivity, allowing Angular to render views without Zone.js.
19. Cheat Sheet
| API Syntax | Purpose |
|---|---|
mySignal.set(val) |
Replaces the signal value directly. |
mySignal.update(val => next) |
Updates the signal value based on its current value. |
mySignal.asReadonly() |
Exposes the signal value as a read-only stream to consumers. |