Чому ми перейшли з Observables на сигнали в Angular
Привіт! Мене звуть Євген Русаков, я працюю в компанії Сільпо, де займаюся розробкою Front-end рішень. Нещодавно ми вирішили оновити одну з підсистем, яка спочатку використовувала Observables
, але тепер впровадили в її основу сигнали. Я написав цю статтю для розробників, які, можливо, ще не зовсім розуміють, чому команда Angular вирішила їх додати.
Як працюють сигнали в Angular
З виходом сигналів у сімнадцятій версії Angular багато розробників зацікавилися новими можливостями для відстеження станів. Я, чесно кажучи, був серед них. Сигнали працюють за принципом «producer-consumer», що означає, що є джерело, яке зберігає певне значення, і група спостерігачів, які хочуть знати про нього. Коли джерело змінює значення, воно автоматично повідомляє всіх своїх спостерігачів про ці зміни.
Сигнали — це джерело, computed()
виступає як джерело і спостерігач одночасно, а effect()
та templates — лише спостерігачі.
Тепер, напевно, ви запитаєте: навіщо взагалі потрібні сигнали, коли вже є Observables
? Якщо ви знайомі з RxJS, це може нагадати вам про BehaviorSubject
. Проте, на відміну від BehaviorSubject
, сигнали не потребують, щоб спостерігачі підписувалися, щоб отримувати сповіщення про зміни. Якщо коротко, то більше нема потреби обходити граф зверху вниз.
Що каже офіційна документація?
Там вказано, що сигнали — це обгортки, які «запаковують» певне значення. Простіше кажучи, їх можна порівняти зі шкаралупою, яка тримає жовток. Щоб зчитати значення з сигналу, потрібна спеціальна геттер-функція. Є два види сигналів: змінні (або writable) та обчислювальні (або computed), які можна лише читати.
Цей компонент демонструє простий приклад використання writable-сигналів у Angular:
@Component({ ... template: ` <div> <h2>Лічильник: {{ getCounterValue() }}</h2> <button (click)="increaseCounter()">Збільшити</button> </div> `, }) class CounterComponent { // Ініціалізація сигналу для зберігання значення лічильника private readonly currentCount = signal(0); // Метод для збільшення значення лічильника на 1 increaseCounter() { this.currentCount.set(this.currentCount() + 1); } // Метод для отримання поточного значення лічильника getCounterValue() { return this.currentCount(); } }
Сomputed
Розберімось, чим відрізняються computed-сигнали від звичайних.
По-перше — вони створюються на основі звичайних сигналів і використовуються для отримання нових значень, не зберігаючи їх самостійно. Коли значення одного з сигналів змінюється, computed-сигнал автоматично оновлюється.
По-друге — computed-сигнали працюють за принципом «ледачого» обчислення. Це означає, що функція, що виконує обчислення, запускається тільки тоді, коли ви вперше запитуєте значення цього сигналу. Так ви уникаєте непотрібних обчислень, поки значення не знадобиться.
По-третє — Angular самостійно виявляє, які сигнали є залежностями. Якщо будь-який із них змінюється, computed-сигнал автоматично оновлюється.
І останнє, що важливо пам’ятати: computed-сигнали не можна змінювати. Якщо ви спробуєте присвоїти значення такому сигналу, отримаєте помилку компіляції.
Подивімось на приклад:
const carBrand = signal('Toyota'); const carModel = signal('Camry'); const carFullName = computed(() => `${carBrand()} ${carModel()}`); // Виведе: Toyota Camry console.log(carFullName()); // Зміна моделі автомобіля призводить до перерахунку повної назви carModel.set('RAV'); // Виведе: Toyota RAV console.log(carFullName());
Effect
Згадаймо, що таке ефект. Ефект — це дія, яка відбувається кожного разу, коли щось змінюється.
В контексті сигналів ефект запускається хоча б один раз і повторюється щоразу, коли змінюються сигнали, з якими він пов’язаний. Також важливо пам’ятати, що ефекти виконуються асинхронно під час перевірки змін.
Подивімось на прикладі:
const counterValue = signal(0); effect(() => { // Виведе: "Поточне значення лічильника: 0" console.log(`Поточне значення лічильника: ${counterValue()}`); }); // effect виведе: "Поточне значення лічильника: 2" counterValue.set(2);
В яких кейсах ефекти будуть корисні?
- Логування.
- Синхронізація даних зі стореджами.
- Маніпуляції з DOM.
- Не рекомендується, але можна використовувати для оновлення даних інших сигналів.
Але з останнім пунктом слід бути особливо обережним, оскільки це може призвести до помилок типу ExpressionChangedAfterItHasBeenChecked, нескінченних циклічних оновлень або надмірних циклів перевірки змін.
За замовчуванням Angular забороняє встановлювати значення сигналів всередині ефектів. Це можна дозволити, якщо необхідно, встановивши прапорець allowSignalWrites
під час створення ефекту.
const number = signal(0); const doubledNumber = signal(0); effect(() => { doubledNumber.set(baseNumber() * 2); }, { allowSignalWrites: true }); baseNumber.set(1); // Виведе: 2 console.log(doubledNumber());
Замість використання ефектів для оновлення стану краще застосовувати computed-сигнали для створення стану, який залежить від інших значень. Такий підхід допомагає зробити код більш передбачуваним і простішим в управлінні.
Тепер поговоримо, в якому контексті можуть виконуватись ефекти.
За замовчуванням ефект можна створити тільки в контексті ін’єкції, як-от у конструкторі компонента, директиви або сервісу. Щоб створити ефект за межами конструктора, потрібно передати йому екземпляр Injector
через параметри.
class CounterComponent { readonly currentCount = signal(0); private readonly injector = inject(Injector) startLogging(): void { effect(() => { console.log(`Поточне значення: ${this.currentCount()}`); }, { injector: this.injector }); } }
А як можна відстежувати зміни значення одного сигналу та при цьому отримувати результат іншого так, щоб ефект не запускався, коли змінюється останній?
Для цього існує функція untracked
:
const currentCount = signal(0); const incrementValue = signal(0); const totalCount = signal(0); effect(() => { // Використовуємо untracked, щоб уникнути оновлення totalCount при зміні incrementValue const increment = untracked(() => incrementValue()); totalCount.set(currentCount() + increment); }); // Зміна currentCount вплине на totalCount, він стане 5 + 0 = 5 currentCount.set(5); // Зміна incrementValue не викликає оновлення totalCount incrementValue.set(10); // Виведе: 5 console.log(totalCount());
Отже, що ми маємо
Cигнали — це важливий крок, який наближає Angular до роботи без Zone.js і робить управління станом простішим та зрозумілішим. Вони дозволяють легко відстежувати зміни, а ефекти додають реактивності, автоматично реагуючи на оновлення даних.
Це був лише початок розмови про сигнали в Angular. У мене є ще багато цікавого, чим хочу поділитися в наступних статтях. Я поясню, як усе працює «під капотом», чому, скажімо, не потрібно використовувати detectChanges
, а також дам кілька порад, як працювати одночасно з Observables
і сигналами.
22 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів