Модуль 2 · урок 4 з 15
Сигнали
Сигнал — це значення, яке знає, хто його читає. Звучить дрібно, але саме з цього випливає все інше: автоматичний перерахунок похідних значень, точкове перемальовування і зникнення половини хуків життєвого циклу. Це головна ідея сучасного Angular.
Три функції, з яких усе складається
import { signal, computed, effect } from '@angular/core';
const count = signal(0); // 1. джерело: значення, за яким стежать
const double = computed(() => count() * 2); // 2. похідне: рахується саме
effect(() => { // 3. побічна дія: щось робимо у відповідь
console.log('лічильник став', count());
});
count(); // прочитати → 0
count.set(5); // записати нове значення
count.update(v => v + 1); // змінити на основі попереднього → 6
double(); // 12 — перерахувалось саме, нічого викликати не треба
Ключове тут — читання через виклик: count(), а не count. Саме в момент
читання сигнал запам'ятовує, хто його читав. Коли значення зміниться, він скаже про це рівно тим,
хто на нього дивився — і нікому більше.
Сигнали в компоненті
import { Component, signal, computed } from '@angular/core';
@Component({ /* … */ })
export class TaskList {
readonly tasks = signal<Task[]>([]);
readonly query = signal('');
readonly onlyMine = signal(false);
// похідне значення: перераховується, коли змінюється будь-що з трьох
readonly visible = computed(() => {
const q = this.query().toLowerCase().trim();
return this.tasks()
.filter(t => !this.onlyMine() || t.assigneeId === this.currentUserId())
.filter(t => !q || t.title.toLowerCase().includes(q));
});
readonly total = computed(() => this.visible().length);
}
<p>Знайдено: {{ total() }}</p>
@for (task of visible(); track task.id) { <app-task-card [task]="task" /> }
Зверни увагу, чого тут немає: жодного ngOnChanges, жодного ручного перерахунку
й жодної підписки. Ти описав залежність, а не послідовність дій — і в цьому
головна різниця з тим, як писали раніше.
Незмінність: головна пастка
const tasks = signal<Task[]>([]);
// ❌ так сигнал нічого не помітить: масив той самий об'єкт
tasks().push(newTask);
// ✅ новий масив — сигнал бачить зміну
tasks.update(list => [...list, newTask]);
// ✅ зміна одного елемента теж через новий масив
tasks.update(list => list.map(t =>
t.id === id ? { ...t, status: 'done' } : t
));
Це та сама історія про копію значення й копію посилання, що була в конспекті до техдзвінка. Сигнал порівнює значення за посиланням: якщо посилання те саме, для нього нічого не сталось. Тому правило просте — ніколи не мутуй, завжди створюй нове.
effect: коли він потрібен і коли ні
constructor() {
// доречно: побічна дія назовні
effect(() => {
localStorage.setItem('filter', this.query());
});
}
- Запис у
localStorage. - Логування, аналітика.
- Синхронізація з чимось поза Angular: карта, редактор, канвас.
- Ручна робота з DOM, якої не уникнути.
- Усередині рахується значення для іншого сигналу — це
computed. - Усередині
.set()іншого сигналу — майже завжди помилка. - Він потрібен, щоб «оновити шаблон» — шаблон оновиться сам.
- Він робить HTTP-запит — для цього є
resourceі RxJS із модуля 4.
Просте правило: computed — коли ти обчислюєш, effect — коли
ти щось робиш. Якщо всередині effect з'явився set,
зупинись: майже завжди це маскує залежність, яку треба описати через computed.
linkedSignal: значення, яке залежить, але його можна перезаписати
// вибраний рядок таблиці скидається, коли змінився список,
// але користувач може вибрати інший вручну
readonly selected = linkedSignal<Task[], Task | null>({
source: this.visible,
computation: (tasks, prev) => {
const keep = prev?.value && tasks.find(t => t.id === prev.value!.id);
return keep ?? tasks[0] ?? null;
},
});
selected.set(someTask); // а так — вручну
computed перезаписати не можна, звичайний signal не вміє реагувати на
джерело. linkedSignal закриває проміжок між ними — і це рівно той випадок,
який виникне в модулі 5 з вибором рядка в таблиці.
Кілька дрібниць, які знадобляться
// прочитати сигнал, не підписуючись на нього
const snapshot = untracked(() => this.query());
// власне порівняння: не вважати зміною, якщо id той самий
const user = signal(initial, { equal: (a, b) => a.id === b.id });
// сигнал лише для читання назовні
private readonly _state = signal(0);
readonly state = this._state.asReadonly();
Коротка відповідь: сигнали — для стану, RxJS — для подій у часі. Значення, яке просто є
(фільтр, вибраний рядок, режим теми) — сигнал. Потік, у якому важлива послідовність, затримки,
скасування чи гонки запитів (пошук із debounce, HTTP, WebSocket) — RxJS.
Вони не конкуренти: у модулі 4 буде toSignal() і toObservable(),
які переводять одне в інше одним рядком.