Dziś dowiesz się jak korzystać z Angular Signals oraz czy warto na nie przejść w nadchodzącym roku. Oferują prostsze, bardziej czytelne podejście do zarządzania danymi w porównaniu ze spopularyzowanym RxJS. Choć nie są tak rozbudowane, to z pewnością dla prostych komponentów są one rozsądną alternatywą, a już w szczególności dla tradycyjnych zmiennych wykorzystujących mechanizm change detection.
Zaczniemy od przypomnienia na czym polega programowanie reaktywne. Angular Signals są bardzo silnie powiązane z tym paradygmatem programowania.
Czym jest projektowanie reaktywne i dlaczego jest lepsze?
Programowanie reaktywne to podejście do tworzenia oprogramowania, które koncentruje się na reagowaniu na zmiany danych w czasie rzeczywistym. Opiera się na modelu asynchronicznego przepływu danych, w którym aplikacja automatycznie reaguje na zmiany w danych lub zdarzenia. Mówiąc w skrócie: Subskrybujemy zmiany w strumieniu i na każdą zmianę reagujemy wykonaniem jakiejś operacji (metody). Taką subskrypcję piszemy tylko raz, a zmiana wartości zmiennej w dowolnym momencie będzie powodowała wykonanie naszej operacji (np. wyświetlenie alertu z wartością tej zmiennej).
Programowanie imperatywne natomiast polega na pisaniu kodu, który opisuje krok po kroku, jak osiągnąć dany cel. To programista ręcznie steruje przepływem danych, wywołując odpowiednie funkcje lub procedury w konkretnych momentach. Mówiąc w skrócie: Po każdej zmianie wartości zmiennej musimy też pamiętać o wywołaniu funkcji, które chcielibyśmy wykonać (np. wyświetlenie alertu z wartością tej zmiennej).
Zalety programowania reaktywnego
- Czytelność kodu: Kod reaktywny lepiej opisuje logikę aplikacji w złożonych scenariuszach.
- Elastyczność: Łatwo można zmieniać lub rozszerzać strumienie danych.
- Obsługa asynchroniczności: RxJS oraz Angular Signals pozwalają efektywnie pracować z danymi asynchronicznymi bez zagnieżdżonego kodu (tzw. callback hell).
- Lepsza wydajność: Aktualizacje są ograniczone do tych elementów, które rzeczywiście muszą być zmienione.
Czym są Signals?
Signals to nowa funkcja Angulara, która wprowadza reaktywny sposób zarządzania stanem aplikacji. Signals to:
- Reaktywne zmienne, które automatycznie odświeżają dane, gdy ich wartość się zmienia.
- Ściśle powiązane z koncepcją przepływu danych od góry do dołu (unidirectional data flow).
- Łatwiejsze do debugowania i wydajniejsze, ponieważ eliminują konieczność ręcznej subskrypcji/odsubskrypcji, co było wymagane w RxJS.
Główne przewagi Signals nad RxJS
| Feature | Signals | RxJS |
|---|---|---|
| Prostota | Deklaratywne, intuicyjne API | Wymaga wielu operatorów i subskrypcji |
| Subskrypcje | Brak konieczności subskrybowania i zarządzania nimi | Subskrypcje wymagają zarządzania (np. unsubscribe) |
| Debugowanie | Lepsze wsparcie w narzędziach developerskich | Trudniejsze do śledzenia (np. w DevTools) |
| Wydajność | Signals mają mniejsze obciążenie procesora | Subskrypcje RxJS mogą być bardziej kosztowne |
| Złożoność API | Intuicyjne metody get() i set() | Wymaga znajomości operatorów RxJS |
| Asynchroniczność | TAK! Od wprowadzenia resources() | TAK |
Jednym z największych atutów RxJS nad Signals było wykonywanie zadań asynchronicznych. Zmieniło się to za sprawą wprowadzenia resources(), więc można przypuszczać, że Team Angular dąży do całkowitego zastąpienia RxJS przez Signals. Signals to przyszłość, więc o ile na razie ich wykorzystanie jest opcjonalne, to na pewnym etapie będziesz musiał(a) zacząć z nich korzystać.
Na ten moment RxJS jest w pewnych scenariuszach niezastąpiony za sprawą swojej elastyczności, którą daje nam spora lista modyfikatorów. Czy Signals zostaną tak rozbudowane, aby wyeliminować potrzebę korzystania z tej biblioteki w swoich projektach? Jedną z idei twórców Angulara było podejście Out Of The Box - wszystko miało działać od razu po pobraniu, bez potrzeby zaciągania dodatkowych bibliotek. Być może czas na powrót do korzeni.

Jak korzystać z Signals?
Angular Signals wprowadzają cztery kluczowe elementy:
signal- zmienna reaktywnacomputed- zmienna wyliczana na podstawie innych signalseffect- reakcja na zmiany w signalsresources- obsługa operacji asynchronicznych
Przykład wykorzystania signal()
Tradycyjne podejście wykorzystujące mechanizm change-detection:
import { Component } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<div>
<p>Counter: {{ counter }}</p>
<button (click)="increment()">Increment</button>
</div>
`,
})
export class CounterComponent {
counter = 0;
increment() {
this.counter++;
}
}
Nowe podejście z wykorzystaniem Angular Signals:
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<div>
<p>Counter: {{ counter() }}</p>
<button (click)="increment()">Increment</button>
</div>
`,
})
export class CounterComponent {
counter = signal(0);
increment() {
this.counter.update(value => value + 1);
}
}
Przykład wykorzystania computed()
Każda zmiana price bądź taxRate będzie skutkowała wyliczeniem na nowo priceWithTax.
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-product',
template: `
<div>
<p>Base Price: {{ price() }}$</p>
<p>Price with Tax: {{ priceWithTax() }}$</p>
<button (click)="increasePrice()">Increase Price</button>
</div>
`,
})
export class ProductComponent {
price = signal(100);
taxRate = signal(0.2);
// Cena z podatkiem
priceWithTax = computed(() => this.price() * (1 + this.taxRate()));
increasePrice() {
this.price.update(value => value + 10);
}
}
Przykład wykorzystania effect()
Każda zmiana message (albo każdego innego sygnału w tym komponencie) będzie powodowała wykonanie efektu. Możliwe jest utworzenie paru effects, ale nie jest to rozwiązanie zalecane ponieważ wpływa negatywnie na wydajność - i tak każdy z nich zostanie uruchomiony po zmianie jakiegokolwiek sygnału z tego komponentu. Nie ma możliwości utworzenia efektu, który nasłuchuje zmian wyłącznie w jednym, konkretnym sygnale.
import { Component, signal, effect } from '@angular/core';
@Component({
selector: 'app-logger',
template: `
<div>
<p>{{ message() }}</p>
<button (click)="changeMessage()">Change Message</button>
</div>
`,
})
export class LoggerComponent {
message = signal('Hello, world!');
constructor() {
// Nasłuchujemy zmiany message
effect(() => {
console.log(`Message zmieniło się na: ${this.message()}`);
});
}
changeMessage() {
this.message.set('New message!');
}
}
Przykład zarządzania stanem w serwisach
Poniższy kod wygląda dużo czytelniej niż kod napisany z wykorzystaniem RxJS.
import { Injectable, signal, computed } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class CartService {
private items = signal<string[]>([]);
// Publiczny dostęp do stanu
itemsCount = computed(() => this.items().length);
addItem(item: string) {
this.items.update(currentItems => [...currentItems, item]);
}
removeItem(item: string) {
this.items.update(currentItems => currentItems.filter(i => i !== item));
}
getItems() {
return this.items();
}
}
Przykład wykorzystania resources() do stronnicowania
import { Component, signal, resources } from '@angular/core';
@Component({
selector: 'app-post-list',
template: `
<div>
<div *ngFor="let post of postsResource.data || []">
<h3>{{ post.title }}</h3>
<p>{{ post.body }}</p>
</div>
<button (click)="prevPage()" [disabled]="page() === 1">Previous</button>
<button (click)="nextPage()">Next</button>
</div>
`,
})
export class PostListComponent {
page = signal(1);
pageSize = signal(20);
postsResource = resources(() => {
return fetch(`https://jsonplaceholder.typicode.com/posts?_page=${this.page()}&_limit=${this.pageSize}`)
.then(res => res.json());
});
nextPage() {
this.page.update(p => p + 1);
}
prevPage() {
this.page.update(p => (p > 1 ? p - 1 : 1));
}
changePageSize(size: number) {
this.pageSize.set(size);
}
}
Przyznacie, że wygląda to dużo prościej niż wykorzystanie RxJS? Tak robimy to wykorzystując operatory RxJS:
@Component({
selector: 'app-post-list',
template: `
<div>
<div *ngFor="let post of posts$ | async">
<h3>{{ post.title }}</h3>
<p>{{ post.body }}</p>
</div>
<button (click)="prevPage()" [disabled]="page() === 1">Previous</button>
<button (click)="nextPage()">Next</button>
</div>
`,
})
export class PostListComponent {
private _page$ = new BehaviorSubject<number>(1);
protected page$ = this._page$.asObservable();
private _pageSize$ = new BehaviorSubject<number>(20);
protected pageSize$ = this._pageSize$.asObservable();
posts$ = this.pageSize$.pipe(
combineLatestWith(this.page$),
switchMap(([ pageSize, page]) => fetch(`https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=${pageSize}`),
takeUntilDestroyed()
);
nextPage() {
const currentPage = this._page.value;
this._page.next(++currentPage);
}
prevPage() {
const currentPage = this._page.value;
this._page.next(--currentPage);
}
changePageSize(size: number) {
this._pageSize.next(size);
}
}
Wydajność Signals
W porównaniu z tradycyjnym wykorzystaniem mechanizmu Change Detection
Angular korzysta z mechanizmu stref (zone.js) i sprawdza całe drzewo komponentów podczas cyklu wykrywania zmian (Change Detection Cycle). Nawet jeśli zmiana dotyczy tylko jednego miejsca w aplikacji Angular przeładuje wszystkich rodziców danego komponentu, aby sprawdzić czy te komponenty także nie wymagają odświeżenia.
Sygnały w Angularze działają w sposób reaktywny - zmiany są lokalne i propagują się tylko tam, gdzie są faktycznie potrzebne. Dzięki temu Angular od razu wie, które fragmenty drzewa komponentów powinny zostać zaktualizowane, co eliminuje konieczność przetwarzania całego drzewa.
W porównaniu z wykorzystaniem RxJS
Signals eliminują wiele problemów związanych z RxJS, np.:
- Mniej subskrypcji: Signals automatycznie zarządzają reaktywnością i nie wymagają subskrybowania.
- Brak konieczności odsubskrybowania: Signals nie tworzą subskrypcji RxJS, dzięki czemu nie musisz zarządzać ich życiem w cyklu komponentu.
- Lepsza wydajność w szablonach: Signals są bardziej wydajne w szablonach Angulara, ponieważ Angular optymalizuje ich śledzenie.

Czy Signals zastąpią RxJS?
Nie, RxJS nigdy nie był częścią ekosystemu Angular i to czy chcecie go używać w swoich projektach zależy tylko od Was. Dodanie Signals do Angulara sprawia, że niektórzy z Was być może zdecydują się usunąć RxJS ze swoich projektów, ale nikt tej zmiany na Was nie wymusza. Co więcej, zespół Angular dodał metody, które pozwalają zintegrować sygnały z RxJS takie jak toSignal i toObservable. Przykłady ich wykorzystania znajdziecie w oficjalnej dokumentacji.
Podsumowanie
Największą zaletą konkurencyjnego React’a był szybki start projektu i większa wydajność. Po paru krótkich poradnikach młody programista mógł już napisać swoją pierwszą, szybką aplikację. Aby osiągnąć podobną wydajność w Angular konieczne było wprowadzenie elementów programowania reaktywnego z wykorzystaniem biblioteki RxJS. Ta wiedza była domeną midów i seniorów. Próg wejścia dla juniorów był zbyt duży. Twórcy Angular postanowili to zmienić i ułatwić pisanie wydajnych aplikacji także juniorom - za pomocą Signals.
Czy warto przejść na Signals jako doświadczony programista Angular? Z pewnością, aby ułatwić wejście do projektu młodszym kolegom, ale także dlatego, że Team Angular będzie próbował zachęcić doświadczonych programistów do porzucenia RxJS wprowadzając do core’a kolejne funkcjonalności zastępujące popularne operatory z RxJS. Warto mieć też na uwadze fakt, że Signals są nieco szybsze w szablonach niż async pipe i z pewnością bardziej czytelne niż BehaviorSubject.



