1. Краткая история реактивности
Angular всегда был реактивным фреймворком, но его реактивность работала "под капотом" через Change Detection (запускалась, когда происходили события, асинхронные операции и т. д.). Для сложных сценариев приходилось использовать RxJS Observables — мощный, но не самый простой инструмент для новичков.
Однако с ростом требований к производительности (и количества головной боли у разработчиков) стало понятно: нужен более интуитивный и предсказуемый способ отслеживать изменения данных и автоматически обновлять интерфейс. Тут и появляются Signals — вдохновлённые опытом SolidJS, React Signals, MobX и других современных подходов.
Аналогия
Представьте, что у вас есть умный термометр на стене. Каждый раз, когда температура меняется, он сам обновляет показания — вам не нужно вручную перерисовывать циферблат. Signals — это такие "умные переменные", которые сами знают, когда пришло время обновить интерфейс.
Что такое Signal?
Signal — это особый тип переменной, которая "умеет" оповещать Angular о своём изменении. Когда вы используете Signal в шаблоне или вычисляете на его основе другие значения, Angular автоматически следит за зависимостями и обновляет только то, что реально изменилось.
- Это реактивные контейнеры для значений.
- Они могут хранить любые данные (число, строку, объект, массив и т. д.).
- При изменении Signal Angular "реагирует" и обновляет только нужные части интерфейса.
- Signals очень легковесны и просты в использовании (никаких подписок, отписок, pipe'ов и прочего "rxjs-колдунства").
Минимальный пример
import { signal } from '@angular/core';
const count = signal(0); // count — это signal<number>, начальное значение 0
console.log(count()); // 0
count.set(42);
console.log(count()); // 42
Пояснение:
- signal(начальноеЗначение) — создаёт новый Signal.
- Вызов count() возвращает текущее значение.
- count.set(новоеЗначение) — обновляет значение и оповещает подписчиков.
2. Создание и использование Signal в компоненте
Давайте посмотрим, как это выглядит в реальном Angular-компоненте.
Пример: Счётчик на Signals
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<button (click)="decrement()">-</button>
<span>{{ count() }}</span>
<button (click)="increment()">+</button>
`
})
export class CounterComponent {
count = signal(0);
increment() {
this.count.set(this.count() + 1);
}
decrement() {
this.count.set(this.count() - 1);
}
}
Что происходит:
- count — это Signal, который хранит текущее значение счётчика.
- В шаблоне для отображения используем count(). Да-да, именно с круглыми скобками — Signal это функция!
- При клике на кнопки вызываются методы, которые обновляют значение сигнала. Angular автоматически обновит <span>.
Почему это удобно?
- Нет необходимости вручную вызывать ChangeDetectorRef.detectChanges().
- Нет подписок и отписок, как с Observables.
- Код максимально простой и похож на обычные переменные.
Как Signal работает "под капотом"?
Когда вы используете Signal в шаблоне или в вычислениях, Angular "запоминает", что компонент зависит от этого сигнала. Если значение сигнала меняется, Angular "запускает" обновление только тех компонентов, которые на него зависят.
Это похоже на магический список подписчиков, но без подписок!
- Signal хранит значение и список зависимых вычислений/шаблонов.
- При обновлении значения Signal оповещает только те части приложения, которым это действительно нужно.
3. Вычисляемые сигналы (computed)
Одна из мощнейших фишек Signals — это возможность создавать вычисляемые значения, которые автоматически обновляются, если изменились их зависимости.
Пример: Двойной счётчик
import { signal, computed } from '@angular/core';
const count = signal(2);
const double = computed(() => count() * 2);
console.log(double()); // 4
count.set(5);
console.log(double()); // 10
Пояснение:
- computed(() => ...) — создаёт новый сигнал, который автоматически пересчитывается при изменении зависимостей.
- В данном случае, если count изменился, double тоже обновится.
Применение в компоненте
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-double-counter',
template: `
<div>Обычный счётчик: {{ count() }}</div>
<div>Удвоенный: {{ double() }}</div>
<button (click)="count.set(count() + 1)">+</button>
`
})
export class DoubleCounterComponent {
count = signal(1);
double = computed(() => this.count() * 2);
}
4. Эффекты (effect): реакция на изменение сигнала
Иногда нужно выполнить "побочный эффект" (например, отправить данные на сервер или вывести в консоль), когда сигнал меняется. Для этого есть функция effect.
Пример: Логирование изменений
import { signal, effect } from '@angular/core';
const count = signal(0);
effect(() => {
console.log('Значение count изменилось:', count());
});
count.set(1); // В консоли: Значение count изменилось: 1
count.set(2); // В консоли: Значение count изменилось: 2
Пояснение:
- effect автоматически "подписывается" на все сигналы, которые используются внутри него.
- При изменении любого из этих сигналов функция будет вызвана снова.
5. Сигналы и шаблоны: как это выглядит в реальном приложении
Вернёмся к нашему учебному приложению! Допустим, у нас есть сервис, который хранит список задач. Раньше мы хранили массив задач и использовали Subject или BehaviorSubject для реактивности. Теперь мы можем использовать Signals!
Пример: Сервис задач с Signal
import { Injectable, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class TodoService {
todos = signal<string[]>([]);
addTodo(todo: string) {
this.todos.set([...this.todos(), todo]);
}
removeTodo(index: number) {
const newTodos = this.todos().slice();
newTodos.splice(index, 1);
this.todos.set(newTodos);
}
}
Использование в компоненте
import { Component } from '@angular/core';
import { TodoService } from './todo.service';
@Component({
selector: 'app-todo-list',
template: `
<input [(ngModel)]="newTodo" placeholder="Новая задача">
<button (click)="add()">Добавить</button>
<ul>
<li *ngFor="let todo of todoService.todos(); let i = index">
{{ todo }}
<button (click)="remove(i)">Удалить</button>
</li>
</ul>
`
})
export class TodoListComponent {
newTodo = '';
constructor(public todoService: TodoService) {}
add() {
if (this.newTodo.trim()) {
this.todoService.addTodo(this.newTodo.trim());
this.newTodo = '';
}
}
remove(i: number) {
this.todoService.removeTodo(i);
}
}
Обратите внимание:
- В шаблоне мы используем todoService.todos() — круглые скобки обязательны!
- Angular автоматически обновит список задач при изменении сигнала.
6. Сигналы vs Observables: что выбрать?
| Характеристика | Signals | Observables (RxJS) |
|---|---|---|
| Простота | Очень просты, как переменные | Требуют подписок, pipe'ов, unsubscribe |
| Реактивность | Автоматическая, без подписок | Нужно вручную подписываться |
| Сценарии | Локальное, синхронное состояние | Асинхронные потоки, события, таймеры |
| Производительность | Минимум накладных расходов | Может быть overhead на подписки |
| Интеграция | Новая фича Angular (19+) | Классика Angular с 2-й версии |
Когда использовать Signals:
- Для локального состояния компонентов
- Для синхронных вычислений и derived state
- Когда не нужен асинхронный поток (например, HTTP-запросы — всё ещё лучше делать через Observable)
Когда нужны Observables:
- Для асинхронных данных (HTTP, WebSocket, таймеры)
- Для сложных потоков событий и операторов RxJS
7. Типичные ошибки при работе с Signals
Ошибка №1: Забыли скобки при использовании сигнала.
В шаблоне или коде написали count вместо count(). В результате Angular не будет отслеживать зависимость, и вы получите либо ошибку, либо не обновляющийся интерфейс.
Запомните: Signal — это функция!
Ошибка №2: Пытаетесь мутировать объект напрямую.
Например, если у вас signal<{ name: string }>({ name: 'Vasya' }) и вы пишете user().name = 'Petya' — Angular не узнает об изменении!
Правильно:
user.set({ ...user(), name: 'Petya' });
Ошибка №3: Используете Signals для асинхронных источников.
Signals отлично подходят для синхронного состояния. Для асинхронных данных (например, загрузка с сервера) используйте RxJS Observables и обновляйте Signal после получения данных.
Ошибка №4: Смешиваете Signals и Observables без адаптеров.
Если нужно связать Observable и Signal, используйте специальные утилиты (fromObservable, toObservable), чтобы не терять реактивность.
Ошибка №5: Забыли очистить побочные эффекты.
Если в effect вы запускаете таймеры, подписки или что-то ещё "долгоиграющее", не забудьте их очистить (возвращайте функцию очистки из effect).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ