Signals: реактивность нового поколения

Сигнал — это коробка со значением, которая знает, кто на неё смотрит, и сама будит зависимых, когда содержимое меняется.

«Раньше Angular проверял всё подряд на всякий случай. Сигналы говорят ему точно: вот это изменилось, обнови только это».

Сигналы — главное нововведение современного Angular и будущее его реактивности. Сигнал хранит значение и оповещает всех, кто от него зависит, когда значение меняется. Создаётся он функцией signal(), читается вызовом, а пишется через set или update:

import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <p>Счёт: {{ count() }}</p>
    <button (click)="inc()">+1</button>
    <button (click)="reset()">Сброс</button>
  `,
})
export class CounterComponent {
  count = signal(0);              // создаём сигнал
  inc()   { this.count.update(n => n + 1); }  // на основе старого
  reset() { this.count.set(0); }              // напрямую
}

Заметьте: в шаблоне мы пишем count() — со скобками, ведь чтение сигнала это вызов функции. Когда сигнал в шаблоне меняется, Angular точно знает, какой кусок DOM обновить, без проверки всего дерева компонентов. Это делает приложение быстрее и предсказуемее.

Как работает под капотом

Когда шаблон читает count(), Angular запоминает: «этот участок DOM зависит от сигнала count». Это называется отслеживание зависимостей. При вызове set/update сигнал помечает зависимых как «грязных» и планирует их обновление. Никаких ручных подписок и отписок — граф зависимостей строится автоматически.

   signal(0)  <- коробка со значением + список читателей
        |
   шаблон читает count() -> подписался автоматически
        |
   count.set(5)
        |
   сигнал: "я изменился" -> будит читателей -> обновить DOM-узел

Запускаемая врезка: реактивный сигнал на замыканиях

Соберём сигнал с нуля на чистом JS — это и есть его суть. «Попробуй сам ▶».

// сигнал: значение + подписчики, оповещаемые при изменении
function signal(initial) {
  let value = initial;
  const subscribers = new Set();
  function read() { return value; }
  read.set = (v) => {
    if (v === value) return;        // не изменилось — молчим
    value = v;
    subscribers.forEach(fn => fn(v)); // будим зависимых
  };
  read.update = (fn) => read.set(fn(value));
  read.subscribe = (fn) => subscribers.add(fn);
  return read;
}

const log = [];
const count = signal(0);
count.subscribe(v => log.push('DOM: счёт = ' + v));

count.update(n => n + 1);  // 1
count.update(n => n + 1);  // 2
count.set(2);              // то же значение — оповещения не будет

console.log('текущее:', count()); // 2
console.log(log);                 // ["DOM: счёт = 1", "DOM: счёт = 2"]

Частые ошибки

  • Забыть скобки. {{ count }} выведет функцию; правильно {{ count() }}.
  • Мутировать значение напрямую. Для объектов/массивов меняйте через update с новой ссылкой, иначе сигнал не заметит изменения.
  • Звать set в цикле без нужды. Каждое изменение планирует обновление; группируйте логику.

Best practices

  • Используйте update, когда новое значение зависит от старого, и set — когда нет.
  • Держите в сигналах неизменяемые данные: новый объект вместо мутации старого.
  • Сигналы — предпочтительный способ хранить состояние компонента в современном Angular.

Итоги. Сигнал хранит значение и сам оповещает зависимых. signal() создаёт, () читает, set/update пишут. Это точечная реактивность вместо тотальной проверки. Дальше — производные значения через computed.

Закрепляем

Сигнал — это контейнер значения, который знает своих читателей и сам оповещает их при изменении. Эта простая идея переворачивает реактивность Angular: вместо того чтобы периодически проверять всё дерево компонентов «на всякий случай», фреймворк точно знает, что именно изменилось и какой участок DOM нужно обновить. Отсюда и скорость, и предсказуемость. Запомните троицу операций: signal() создаёт, вызов со скобками () читает, а set и update записывают.

Важнейшая привычка при работе с сигналами — иммутабельность для объектов и массивов. Сигнал замечает изменение по смене ссылки, а не по содержимому. Если вы мутируете массив на месте через push или меняете поле объекта напрямую, ссылка остаётся прежней, и сигнал «не видит» изменения — зависимые computed и шаблоны не обновятся. Поэтому всегда создавайте новый массив или объект: update(list => [...list, item]) вместо list.push(item). Эта дисциплина поначалу кажется лишней, но именно она делает реактивность надёжной.

ОперацияНазначение
signal(v)Создать сигнал
count()Прочитать значение
count.set(v)Записать новое значение
count.update(fn)Изменить на основе старого
Проверьте себя
1. Как прочитать текущее значение сигнала count в шаблоне?
A{{ count }}
B{{ count() }}
C{{ count.value }}
D{{ get(count) }}
2. Когда лучше использовать update() вместо set()?
AНикогда
BКогда новое значение вычисляется на основе старого (например count + 1)
CТолько для строк
DКогда нужно очистить сигнал