Signals: signal, computed, effect
A signal is a wrapper around a value that tells Angular when the value has changed. Angular knows which parts of the page read the signal and updates only those. Signals are the main way to keep component state in modern Angular.
Creating and reading
The signal() function creates a signal with an initial value. To read the value, call the signal like a function:
// src/app/todo-list/todo-list.ts
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-todo-list',
templateUrl: './todo-list.html',
})
export class TodoList {
count = signal(0);
}<!-- src/app/todo-list/todo-list.html -->
<p>Tasks: {{ count() }}</p>The page shows "Tasks: 0". The parentheses after the name are required — both in the template and in the class.
Changing: set and update
set() writes a new value, update() computes it from the old one:
// src/app/todo-list/todo-list.ts
export class TodoList {
count = signal(0);
increment() {
this.count.update((n) => n + 1);
}
reset() {
this.count.set(0);
}
}<!-- src/app/todo-list/todo-list.html -->
<p>Tasks: {{ count() }}</p>
<button (click)="increment()">+1</button>
<button (click)="reset()">Reset</button>Each click on "+1" increases the number on the page, "Reset" brings it back to zero.
A signal with an array
For the to-do list the signal holds an array of objects. Describe the task type in a separate file src/app/todo.ts:
// src/app/todo.ts
export interface Todo {
id: number;
title: string;
done: boolean;
}// src/app/todo-list/todo-list.ts
import { Component, signal } from '@angular/core';
import { Todo } from '../todo';
@Component({
selector: 'app-todo-list',
templateUrl: './todo-list.html',
})
export class TodoList {
todos = signal<Todo[]>([
{ id: 1, title: 'Buy bread', done: true },
{ id: 2, title: 'Call Oleg', done: false },
]);
private nextId = 3;
add(title: string) {
const todo = { id: this.nextId++, title, done: false };
this.todos.update((list) => [...list, todo]);
}
}update() returns a new array via spread — this way Angular sees that the value has changed.
computed
computed() creates a read-only signal that is derived from other signals and recalculates by itself:
// src/app/todo-list/todo-list.ts
import { computed } from '@angular/core';
export class TodoList {
// ...todos and add() as above
left = computed(() => this.todos().filter((t) => !t.done).length);
}<!-- src/app/todo-list/todo-list.html -->
<p>Left to do: {{ left() }}</p>It shows "Left to do: 1". After you add a task, the number grows without a single line of update code.
effect
effect() runs an action every time the signals it reads change. Create it in the constructor or in a field declaration:
// src/app/todo-list/todo-list.ts
import { effect } from '@angular/core';
export class TodoList {
// ...todos as above
constructor() {
effect(() => {
localStorage.setItem('todos', JSON.stringify(this.todos()));
});
}
}The list is saved to localStorage on every change. Effects are for talking to the outside world; compute values for display with computed.
Common mistake
Changing the array inside a signal directly: this.todos().push(task). The array reference stays the same, so the signal does not learn about the change, and computed does not recalculate. Always create a new array with update() and spread.