Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Сигнали в Angular». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Сигнали Angular (Signals)** - це реактивні контейнери стану, представлені в Angular 16. Вони автоматично відстежують залежності й оновлюють лише ті частини UI, які читають змінене значення. Zone.js не потрібен. `signal()` для стану, `computed()` для похідних значень, `effect()` для побічних ефектів. ```typescript count = signal(0); double = computed(() => this.count() * 2); increment() { this.count.update(v => v + 1); } ``` **Ключове:** перерендерюються лише байндінги, що читають змінений сигнал.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Сигнали Angular (Signals)** - це реактивні контейнери стану, представлені в Angular 16. Вони автоматично відстежують залежності і оновлюють лише ті частини UI, які насправді читають змінене значення. ## Теорія ### Коротко - Сигнали схожі на клітинку в Excel: змінюєш A1 - перераховуються лише клітинки, що посилаються на A1, а не весь аркуш - Zone.js проходив по всьому дереву компонентів після кожної події; Signals надсилають оновлення лише по записаному графу залежностей - Три примітиви: `signal()` зберігає стан, `computed()` виводить значення, `effect()` запускає побічні ефекти - Новий застосунок на Angular 16+: Signals для стану, RxJS - для HTTP, WebSocket і таймерів - Міст між двома підходами: `toSignal()` обгортає Observable, `toObservable()` - навпаки ### Швидкий приклад ```typescript import { Component, signal, computed } from '@angular/core'; @Component({ template: ` <p>Лічильник: {{ count() }}</p> <p>Подвоєне: {{ double() }}</p> <button (click)="increment()">+</button> ` }) export class CounterComponent { count = signal(0); // writable signal double = computed(() => this.count() * 2); // перераховується лише коли count змінюється increment() { this.count.update(v => v + 1); // атомарне оновлення } } ``` Натискаєш "плюс": `count` стає 1, `double` - 2. Перерендерюються лише ці два байндінги. Решта дерева компонентів не зачіпається. ### Головна різниця від Zone.js Zone.js патчить браузерні API і запускає повний обхід дерева компонентів після кожної події, таймауту або HTTP-відповіді. Signals будують граф залежностей під час читання: коли шаблон або `computed()` викликає `count()`, Angular фіксує цей зв'язок. На `set()` або `update()` планувальник проходить лише по зафіксованих ребрах і ставить мікрозадачу в чергу через `queueMicrotask`. Zone.js не потрібен. Бенчмарки Angular показують прискорення в 2-10 разів на великих деревах компонентів при роботі в Zone-less режимі. ### Коли використовувати - Локальний стан компонента (поле форми, перемикач, лічильник) → `signal()`, мінімум коду - Спільний стан застосунку → Signal в `@Injectable` сервісі, ін'єктуй де потрібно - Значення, що залежать від іншого стану → `computed()`, кешується і обчислюється ліниво - HTTP-запити, WebSocket, таймери → RxJS, потім `toSignal()` для шаблонів - Існуюча кодова база на Zone.js → Signals працюють поруч, мігруй поступово ### Як це працює всередині `signal(0)` створює вузол у реактивному графі. Виклик `count()` всередині `computed()` або байндінгу шаблону записує ребро від читача до вузла сигналу. При `count.set(1)` планувальник позначає всі залежні вузли як "брудні" і ставить мікрозадачу в чергу. Під час виконання черги Angular переобчислює лише брудні вузли. Результати `computed()` кешуються: якщо нічого вище по графу не змінилось, повертається закешоване значення без повторного запуску функції. `effect()` працює так само. Він повторно запускається, коли змінюється будь-який сигнал, прочитаний під час попереднього виконання. Перед повторним запуском Angular викликає `onCleanup()`, зареєстрований в попередньому циклі. На практиці я бачив, як команди годинами дебажили проблеми з Zone.js, які повністю зникали після переносу стану в Signal. ### Signals проти Observables | | Signals | Observables (RxJS) | |---|---|---| | Завжди має значення | Так | Ні (потрібен `BehaviorSubject`) | | Синхронне читання | Так | Може бути асинхронним | | Автоочищення | Так | Ручний `unsubscribe()` | | Синтаксис у шаблоні | `{{ count() }}` | `{{ count$ \| async }}` | | Оператори | Відсутні | Повний набір RxJS | | Призначення | Синхронний стан | Асинхронні потоки подій | ### Типові помилки **1. Забув `()` при читанні** ```typescript {{ count }} // виведе об'єкт Signal, без реактивності {{ count() }} // правильно: реєструє залежність ``` **2. Читання Signal в конструкторі** ```typescript constructor() { console.log(this.count()); // виконається один раз, більше не оновиться } // Виправлення: перенеси читання в effect() або lifecycle hook ``` **3. `set` з ручним читанням замість `update`** ```typescript count.set(count() + 1); // два читання, ризик застарілого значення count.update(c => c + 1); // правильно: атомарна трансформація ``` **4. Пряма мутація масиву-сигналу** ```typescript items.value.push('нове'); // сповіщення не надійде items.update(arr => [...arr, 'нове']); // правильно: нове посилання сповіщає залежних ``` **5. Застарілі замикання в effects** ```typescript externalValue = 10; // Неправильно: externalValue захоплюється один раз, не перевідстежується effect(() => console.log(this.count() * externalValue)); // Правильно: читай властивість класу всередині effect effect(() => console.log(this.count() * this.externalValue)); ``` ### Де зустрічається в реальних проектах - NgRx 17+ перейшов на Signals для селекторів, замінивши RxJS `map`-ланцюги - Angular Material 16+ таблиці приймають Signal-масив як `dataSource` - Angular 17 представив `input.required<string>()` як Signal-заміну для `@Input()` - PrimeNG використовує `toSignal()` для переведення HTTP Observable в шаблони - Nx workspace застосовують Signal-сервіси для стану між мікрофронтендами ### Питання на співбесіді **Q:** Яка різниця між writable signal і computed signal? **A:** Writable signal (`signal(0)`) надає методи `set()` і `update()` для зміни значення. `computed()` - лише для читання, він виводить значення з інших сигналів автоматично; виклик `set()` на ньому кидає помилку. **Q:** Як Signals покращують продуктивність порівняно з Zone.js? **A:** Zone.js запускає повний обхід дерева після кожної асинхронної події. Signals оновлюють лише брудні вузли графа залежностей. Бенчмарки Angular фіксують прискорення в 2-10 разів на великих застосунках у Zone-less режимі. **Q:** Коли `computed()` перераховується? **A:** Лише коли хоча б один upstream сигнал змінився І обчислене значення фактично прочитане. Якщо ніхто не читає - перерахунок відкладається. Результат кешується між читаннями. **Q:** Що робить `effect()` між запусками? **A:** Перед кожним повторним запуском Angular викликає `onCleanup()` з попереднього циклу. Використовуй його для скасування таймерів, обриву запитів або видалення слухачів подій. **Q:** Як Signals запобігають UI tearing при пакетних оновленнях в Angular 18+ без Zone.js? **A:** Планувальник об'єднує кілька `set()` в один flush мікрозадач. Всі читання `computed()` під час цього flush бачать узгоджений знімок усіх значень - часткові стани ніколи не потрапляють у шаблон. ## Приклади ### Базовий лічильник із похідним станом ```typescript import { Component, signal, computed } from '@angular/core'; @Component({ selector: 'app-counter', template: ` <p>Лічильник: {{ count() }}</p> <p>Статус: {{ status() }}</p> <button (click)="increment()">+</button> <button (click)="reset()">Скинути</button> ` }) export class CounterComponent { count = signal(0); status = computed(() => this.count() % 2 === 0 ? 'парний' : 'непарний'); increment() { this.count.update(c => c + 1); } reset() { this.count.set(0); } } ``` `status` перераховується лише при зміні `count`. Не при кожній події в застосунку, лише на точну залежність. ### Спільний стан у сервісі ```typescript // todo.service.ts import { Injectable, signal, computed } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class TodoService { private todos = signal<string[]>([]); count = computed(() => this.todos().length); completed = computed(() => this.todos().filter(t => t.startsWith('[x]')).length); addTodo(text: string) { this.todos.update(list => [...list, text]); } removeTodo(index: number) { this.todos.update(list => list.filter((_, i) => i !== index)); } } // В будь-якому компоненті: // service = inject(TodoService); // шаблон: {{ service.count() }} завдань, {{ service.completed() }} виконано ``` Будь-який компонент, що читає `service.count()`, оновиться автоматично. Жодного Subject, жодного `async` пайпу, жодного ручного `unsubscribe`. ### Input signals із computed-валідацією (Angular 17+) ```typescript import { Component, input, computed } from '@angular/core'; @Component({ selector: 'app-user-card', template: ` <p>{{ name() }}</p> <span *ngIf="isAdult()">18+</span> <p>{{ roleLabel() }}</p> ` }) export class UserCardComponent { name = input.required<string>(); age = input(0); role = input<'admin' | 'user'>('user'); isAdult = computed(() => this.age() >= 18); roleLabel = computed(() => this.role() === 'admin' ? 'Адміністратор' : 'Учасник'); } ``` В батьківському шаблоні: `<app-user-card [name]="'Alice'" [age]="25" [role]="'admin'" />`. Всі три inputs - це Signals під капотом, тому `computed()` відстежує їх без зайвого коду і без `ngOnChanges`.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.