Sygnały w Angularze 16 - część 1
Angular 16 przyniósł sporo nowości, ale jedną z tych, o których rozmawia się najczęściej, są sygnały. Jeśli śledzisz angularową społeczność, to pewnie już widziałeś posty o signal(), computed() i effect(). W tym artykule wyjaśnię, czym w ogóle są sygnały, skąd się wzięły i jak zacząć ich używać.
Skąd wziął się ten pomysł?
Żeby zrozumieć sygnały, warto cofnąć się o krok i zastanowić, jaki problem próbują rozwiązać.
Angular od zawsze używał mechanizmu wykrywania zmian (change detection) opartego na Zone.js. W skrócie: Zone.js "łata" natywne API przeglądarki (setTimeout, fetch, eventy DOM itd.) i informuje Angulara, że coś się mogło zmienić. Angular wtedy sprawdza cały drzewo komponentów i aktualizuje widok. Działa, ale ma wady - przy dużych aplikacjach może być wolne, a samo Zone.js to dodatkowa warstwa magii, przez którą czasem trudno zrozumieć, co się dzieje.
Frameworki takie jak Solid.js czy Vue 3 od jakiegoś czasu używają innego podejścia - reaktywności opartej na sygnałach. Angular postanowił pójść w tym kierunku. Sygnały w Angularze 16 są na razie w fazie developer preview, ale już działają i można ich używać.
Czym właściwie jest sygnał?
Sygnał to po prostu opakowanie na wartość, które wie, kiedy ta wartość się zmieniła i potrafi poinformować o tym wszystkich zainteresowanych.
Brzmi banalnie, ale to właśnie o to chodzi. Zamiast liczyć na to, że Zone.js wykryje zmianę, dajesz Angularowi precyzyjną informację: "ta konkretna wartość się zmieniła, odśwież to, co jej używa".
import { signal } from '@angular/core';
const count = signal(0);
console.log(count()); // 0Kilka rzeczy do zapamiętania od razu:
Po pierwsze, sygnał tworzy się przez wywołanie funkcji signal() z wartością początkową. Po drugie, żeby odczytać wartość sygnału, wywołujesz go jak funkcję - count(), nie count. To trochę nieoczywiste na początku, ale szybko się do tego przyzwyczajasz.
Jak zmienić wartość sygnału?
Są dwa sposoby.
Pierwszy to set() - po prostu podajesz nową wartość:
count.set(5);
console.log(count()); // 5Drugi to update() - przydaje się wtedy, gdy nowa wartość zależy od starej:
count.update(value => value + 1);
console.log(count()); // 6Jeśli sygnał przechowuje obiekt, możesz też użyć mutate() (dostępne w Angular 16, w późniejszych wersjach zastąpione przez update()):
const user = signal({ name: 'Jan', age: 25 });
user.mutate(u => {
u.age = 26;
});Sygnały w komponencie - prosty przykład
Zobaczmy, jak to wygląda w praktyce. Klasyczny przykład licznika:
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<p>Wartość: {{ count() }}</p>
<button (click)="increment()">+</button>
<button (click)="decrement()">-</button>
<button (click)="reset()">Reset</button>
`
})
export class CounterComponent {
count = signal(0);
increment() {
this.count.update(v => v + 1);
}
decrement() {
this.count.update(v => v - 1);
}
reset() {
this.count.set(0);
}
}W szablonie odczytujesz wartość tak samo jak w kodzie TypeScript - count(). Angular wie, że ten szablon zależy od sygnału count, i odświeży go automatycznie przy każdej zmianie. Bez Zone.js, bez ręcznych subskrypcji, bez ChangeDetectorRef.
Sygnały pochodne - computed()
Często chcesz mieć wartość, która jest wyliczana na podstawie innej wartości. Do tego służy computed():
import { signal, computed } from '@angular/core';
const price = signal(100);
const quantity = signal(3);
const total = computed(() => price() * quantity());
console.log(total()); // 300
price.set(150);
console.log(total()); // 450computed() tworzy sygnał tylko do odczytu - nie możesz wywołać na nim set() ani update(). Jego wartość jest przeliczana automatycznie, gdy zmieni się którykolwiek z sygnałów, od których zależy. Co ważne, Angular jest sprytny i nie przelicza wartości, jeśli nikt jej w danym momencie nie potrzebuje - to tzw. lazy evaluation.
Przykład w komponencie:
@Component({
selector: 'app-cart',
template: `
<p>Cena: {{ price() }} zł</p>
<p>Ilość: {{ quantity() }}</p>
<p>Razem: {{ total() }} zł</p>
<button (click)="addOne()">Dodaj sztukę</button>
`
})
export class CartComponent {
price = signal(49);
quantity = signal(1);
total = computed(() => this.price() * this.quantity());
addOne() {
this.quantity.update(q => q + 1);
}
}Czym sygnały NIE są
Warto to powiedzieć wprost, bo łatwo się pomylić.
Sygnały to nie to samo co EventEmitter. EventEmitter w Angularze służy do komunikacji między rodzicem a dzieckiem przez @Output() - to zupełnie inny mechanizm.
Sygnały to nie to samo co Observable z RxJS. Obserwable są strumieniami zdarzeń, które rozwijają się w czasie. Sygnał to zawsze ma aktualną wartość, którą możesz odczytać w dowolnym momencie.
Sygnały to nie zastępstwo dla serwisów. Serwisy nadal mają sens, a sygnały w serwisach działają świetnie - ale o tym w drugiej części.
Dlaczego to ma sens?
Porównaj dwa podejścia. Stare, z propertyami i change detection:
export class OldComponent {
count = 0;
increment() {
this.count++;
// Angular musi sprawdzić cały komponent żeby wiedzieć, że coś się zmieniło
}
}Nowe, z sygnałami:
export class NewComponent {
count = signal(0);
increment() {
this.count.update(v => v + 1);
// Angular wie dokładnie, że `count` się zmieniło i co trzeba odświeżyć
}
}Przy większych aplikacjach ta różnica robi się odczuwalna - Angular może odświeżać tylko te fragmenty widoku, które faktycznie zależą od zmienionego sygnału, zamiast sprawdzać wszystko.
Kontynuacja w części 2
W części drugiej zajmiemy się effect(), czyli reakcjami na zmiany sygnałów, użyciem sygnałów w serwisach oraz tym, jak sygnały współpracują z RxJS - bo te dwa światy wcale nie muszą ze sobą walczyć.
Komentarze użytkowników