How do computed signals work?
Angular computed signals are a powerful feature within the new reactivity model, allowing you to derive values from other signals. They automatically re-evaluate only when their dependencies change, offering significant performance benefits and simplifying state management.
What are Computed Signals?
A computed signal is a special type of signal that derives its value from one or more existing signals. Unlike writable signals, which hold a direct value that can be set, computed signals define a calculation function that is automatically executed whenever any of its input signals change. The result of this calculation becomes the value of the computed signal.
Syntax and Basic Usage
You create a computed signal using the computed() function from @angular/core. It takes a function as an argument, which defines how the computed value is derived. Inside this function, you 'read' other signals by calling them like functions, and Angular automatically tracks these dependencies.
import { signal, computed } from '@angular/core';
const quantity = signal(1);
const price = signal(100);
const total = computed(() => quantity() * price());
console.log(total()); // 100
quantity.set(2);
console.log(total()); // 200 (automatically re-evaluated)
Key Characteristics
- Derived Values: They always represent a value calculated from other signals, never holding a directly set value.
- Memoization: Computed signals are memoized. Their calculation function is only executed when one of their input dependencies changes. If you read a computed signal multiple times without its dependencies changing, the cached value is returned, avoiding unnecessary re-computations.
- Lazily Evaluated: A computed signal's value is not calculated until it is read for the first time. Subsequent reads use the memoized value until dependencies change.
- Read-only: The value of a computed signal cannot be directly set. It is always derived from its inputs.
- Automatic Dependency Tracking: Angular automatically detects which signals are read inside the
computed()function and establishes dependencies. When any of these dependencies update, the computed signal is marked as stale and will re-evaluate upon its next read.
Benefits of Using Computed Signals
- Performance Optimization: Memoization prevents redundant calculations, improving application performance, especially in complex UIs with many derived values.
- Simplified Logic: They encapsulate derivation logic, making components cleaner and easier to reason about.
- Enhanced Readability: The explicit declaration of derived values clarifies data flow.
- Fine-grained Reactivity: Only the parts of the UI that depend on a changed signal (or a computed signal derived from it) are updated, leading to more efficient change detection.
When to Use Computed Signals
Use computed signals whenever you need to calculate a value based on the current state of one or more other signals, and you want that calculated value to automatically update whenever its inputs change. Common use cases include filtering lists, calculating aggregates (sums, averages), formatting data, or determining UI state (e.g., whether a button should be enabled).
import { signal, computed } from '@angular/core';
interface Todo {
id: number;
text: string;
completed: boolean;
}
const todos = signal<Todo[]>([
{ id: 1, text: 'Learn signals', completed: false },
{ id: 2, text: 'Build an app', completed: true }
]);
const completedTodos = computed(() =>
todos().filter(todo => todo.completed)
);
const pendingTodosCount = computed(() =>
todos().filter(todo => !todo.completed).length
);
console.log('All todos:', todos());
console.log('Completed todos:', completedTodos());
console.log('Pending count:', pendingTodosCount());
todos.update(currentTodos => [
...currentTodos,
{ id: 3, text: 'Deploy to production', completed: false }
]);
console.log('All todos after update:', todos());
console.log('Completed todos after update:', completedTodos());
console.log('Pending count after update:', pendingTodosCount());