Kurs Angular · Moduł 6: Signals

Wieża i goniec - sygnały obok strumieni

18 min czytania ·
W tej lekcji9

Przez cały ten moduł stawiałeś wieże sygnałowe. Sygnał odpowiada na jedno pytanie i robi to natychmiast: ile jest teraz. Wywołasz level() i dostajesz liczbę w tej samej milisekundzie, bez czekania i bez pytania kogokolwiek o zgodę. Tak wygląda stan.

Są jednak wieści, które w to pytanie się nie mieszczą. Dzwon przy głównej bramie Akademii bije raz po raz przez cały dzień - to nie jest jedna wartość, tylko ciąg uderzeń rozłożony w czasie. Goniec wysłany do sąsiedniego zamku wróci za trzy dni albo nie wróci wcale. Zwiadowca na szlaku melduje pięć razy w ciągu popołudnia, a potem milczy do wieczora. Gdybyś chciał opisać takie rzeczy wieżą, musiałbyś zadać pytanie "ile teraz uderzeń dzwonu" i dostać odpowiedź, która nic nie mówi o tym, że kolejne uderzenia dopiero nadejdą.

Jeden taki ciąg już widziałeś przy szlakach Tōkaidō. Kiedy odczytywałeś parametry trasy przez route.paramMap bez migawki, nie dostawałeś wartości, tylko obiekt, na który trzeba się było zapisać metodą subscribe(), żeby Angular wołał Twoją funkcję przy każdej nowej wieści. Ten obiekt nazywa się Observable, po polsku strumień, a cała biblioteka narzędzi do pracy z takimi strumieniami - RxJS - czeka na Ciebie w następnym module, u zwiadowców ninja. Dzisiaj nie uczysz się RxJS. Dzisiaj uczysz się czegoś, bez czego strumienie i sygnały żyłyby w dwóch osobnych zamkach: jak przełożyć jedno na drugie i po co który wybierać.

Jedna drobna konwencja na początek, bo zobaczysz ją w każdym przykładzie. Nazwy zmiennych trzymających strumień kończy się w Angularze znakiem dolara: timer$, query$, events$. To wyłącznie umowa dla ludzkiego oka, kompilator nie zwraca na nią najmniejszej uwagi, ale dzięki niej od pierwszego spojrzenia wiadomo, że w środku siedzi ciąg wartości, a nie pojedyncza liczba.

Most zbudowany własnoręcznie

Zacznijmy od kłopotu, bo to on tłumaczy całą resztę lekcji. Chcesz pokazać w szablonie liczbę uderzeń dzwonu, a uderzenia przychodzą strumieniem. Szablon Angulara nie umie czytać strumienia tak, jak czyta sygnał - nie ma tam nawiasów, które cokolwiek zwrócą. Musisz więc zbudować most: zapisać się na strumień i każdą przychodzącą wartość wpisać do sygnału, który szablon już rozumie.

Do zbudowania strumienia użyję w przykładzie funkcji interval z biblioteki RxJS. Podajesz jej liczbę milisekund, a ona zwraca strumień, który co tyle czasu wysyła kolejną liczbę: zero, jeden, dwa, trzy. To wszystko, co musisz o niej dziś wiedzieć. Resztę poniższego kodu znasz: dekorator @Component, sygnał z signal() i metoda set() z wcześniejszych postojów tego modułu.

import { Component, signal } from '@angular/core';
import { interval } from 'rxjs';

@Component({
  selector: 'app-gate-bell',
  standalone: true,
  template: '<p>Uderzenia dzwonu: {{ ticks() }}</p>'
})
export class GateBellComponent {
  ticks = signal(0);

  constructor() {
    interval(1000).subscribe(value => {
      this.ticks.set(value);
    });
  }
}

Ten kod działa: co sekundę strumień wysyła nową liczbę, subskrypcja ją odbiera, set() wpisuje ją do sygnału, a szablon pokazuje kolejne uderzenie. Zwróć uwagę, czego ta konstrukcja nie zmieniła. Szablon czyta ticks() dokładnie tak samo jak każdy inny sygnał z tego modułu i w ogóle nie wie, że wartość przyjechała z zewnątrz. Cała cena siedzi gdzie indziej: napisałeś ręcznie trzy rzeczy, a nie jedną. Pole na wartość, subskrypcję i - o tym najłatwiej zapomnieć - dług do spłacenia, bo subskrypcję wypada zamknąć przy niszczeniu komponentu, inaczej dzwon bije dalej dla komponentu, którego już nie ma na ekranie. Ten sam most przepisany w dwudziestu komponentach to dwadzieścia okazji, żeby raz o czymś zapomnieć.

toSignal - sygnalista przy bramie

W Akademii nikt nie każe dowódcy osobiście wybiegać po meldunki. Przy bramie stoi sygnalista: odbiera każdy zwój przyniesiony przez gońca i natychmiast przekłada go na dym nad wieżą. Dowódca patrzy wyłącznie na wieżę i nie musi wiedzieć, ilu gońców biegnie szlakiem. Angular ma dokładnie takiego sygnalistę i nazywa się on toSignal().

Funkcja toSignal() przyjmuje strumień i zwraca sygnał, który zawsze pokazuje ostatnią wartość z tego strumienia. Kierunek jest jeden i tylko jeden: ze strumienia do sygnału, z gońca na wieżę. Mieszka ona w osobnym pakiecie, @angular/core/rxjs-interop, a nie w znanym Ci @angular/core - Angular trzyma mostki do RxJS na uboczu, żeby aplikacja, która RxJS nie używa, nie musiała go wciągać. Cała linia importu składa się z czterech kawałków w niezmiennej kolejności: słowo import, klamra z nazwą { toSignal }, słowo from, a na końcu nazwa pakietu w apostrofach '@angular/core/rxjs-interop'.

Drugim, nieobowiązkowym argumentem jest obiekt opcji. Dziś potrzebna Ci z niego jedna pozycja: initialValue, czyli wartość, którą sygnał ma pokazywać, zanim strumień powie cokolwiek.

import { Component } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';

@Component({
  selector: 'app-gate-bell',
  standalone: true,
  template: '<p>Uderzenia dzwonu: {{ ticks() }}</p>'
})
export class GateBellComponent {
  private timer$ = interval(1000);

  ticks = toSignal(this.timer$, { initialValue: 0 });
}

Trzy linie zamieniły się w jedną i to jest cała zmiana widoczna gołym okiem. Znacznie ciekawsze jest to, co nie uległo zmianie. Szablon wygląda znak w znak tak samo, bo ticks to nadal zwyczajny sygnał czytany nawiasami. Strumień też pozostał strumieniem: timer$ niczego o sygnałach nie wie i można go dalej podać komukolwiek innemu. Zniknęły natomiast obie rzeczy, które trzeba było pilnować: subskrypcję zakłada teraz toSignal(), a zamyka ją sama, gdy komponent jest niszczony. Właśnie dlatego to wywołanie musi stać tam, gdzie Angular wie, o czyje niszczenie chodzi - czyli w polu klasy albo w konstruktorze, w tym samym kontekście wstrzykiwania, w którym w module o gildiach wywoływałeś inject().

Warto od razu zapamiętać jeszcze jedno ograniczenie, bo bywa zaskoczeniem. Sygnał zwrócony przez toSignal() jest tylko do odczytu: nie ma na nim ani set(), ani update(). Sprawdziłem to kompilatorem - próba wywołania ticks.set(5) kończy się błędem "Property 'set' does not exist on type 'Signal<number>'" i aplikacja się nie buduje. To nie jest kaprys, tylko konsekwencja: wartość dyktuje strumień, więc gdyby wolno Ci było wpisać do tego sygnału cokolwiek z boku, najbliższy meldunek gońca i tak by to nadpisał. Samo wywołanie ma zawsze ten sam kształt czterech kawałków: nazwa toSignal, nawias otwierający, nazwa strumienia timer$, nawias zamykający.

Kiedy strumień jeszcze nic nie powiedział

Opcja initialValue nie jest obowiązkowa, więc naturalne pytanie brzmi: co pokazuje sygnał, jeśli jej nie podasz, a goniec dopiero wyruszył? Nie zero, nie pustą wartość wymyśloną przez Angulara. W takiej sytuacji toSignal() zwraca sygnał o typie "wartość albo undefined", a pierwszy odczyt daje właśnie undefined.

private timer$ = interval(1000);

ticks = toSignal(this.timer$);

Uruchomiłem oba warianty obok siebie i wynik jest jednoznaczny: wersja bez opcji zaraz po utworzeniu zwraca undefined, a wersja z { initialValue: 0 } zwraca zero. Po dwóch uderzeniach obie pokazują już to samo, bo od pierwszego meldunku różnicy między nimi nie ma żadnej. Zmienił się jednak typ, a to widać w całym komponencie: bez wartości początkowej TypeScript będzie Cię pytał o undefined w każdym miejscu, w którym sięgasz po pole wyniku.

Masz do wyboru trzy wyjścia i mam wśród nich swojego faworyta. Pierwsze to initialValue. Drugie to zostawienie undefined i zasłonięcie widoku warunkiem @if w szablonie. Trzecie, { requireSync: true }, jest dla strumieni, które wysyłają wartość natychmiast przy subskrypcji - wtedy typ jest czysty, ale za cenę ryzyka: sprawdziłem to na strumieniu interval, który natychmiast nie odpowiada, i Angular rzucił błąd NG0601 z komunikatem, że strumień nie wyemitował wartości synchronicznie. Moja rada: dla strumieni ciągłych, takich jak dzwon czy zdarzenia myszy, podawaj initialValue, bo zero uderzeń to prawda o świecie. Dla jednorazowej odpowiedzi z sieci zostaw undefined, bo ono uczciwie znaczy "danych jeszcze nie ma", podczas gdy sztuczna pusta tablica skłamie, że dane już przyszły i są puste.

Meldunek z drugiego końca świata

Najczęstszy strumień w prawdziwej aplikacji nie tyka co sekundę - przynosi jedną odpowiedź z serwera i milknie. Do rozmowy z serwerem służy w Angularze serwis HttpClient, któremu poświęcony jest cały późniejszy moduł o gońcach posłańcach. Dziś wystarczy Ci jedna jego cecha: metoda get() niczego nie zwraca od razu. Oddaje strumień, który wyśle jedną wartość, gdy odpowiedź dotrze, i na tym zakończy. Serwis bierzesz przez inject() z modułu o gildiach, a w nawiasach ostrokątnych po get podajesz typ spodziewanych danych.

Umówmy się najpierw, czym są te dane. Interfejs User opisuje jednego użytkownika: ma pole id typu number, żeby dało się go odróżnić od innych, i pole name typu string. Serwer odda całą ich listę, więc typem odpowiedzi jest User[].

import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal } from '@angular/core/rxjs-interop';

interface User {
  id: number;
  name: string;
}

@Component({
  selector: 'app-users',
  standalone: true,
  template: `
    @if (users()) {
      @for (user of users(); track user.id) {
        <p>{{ user.name }}</p>
      }
    }
  `
})
export class UsersComponent {
  private http = inject(HttpClient);

  users = toSignal(this.http.get<User[]>('/api/users'));
}

Cały komponent to teraz dwa pola i żadnej metody. Warunek @if (users()) nie jest ozdobnikiem - stoi tam dokładnie dlatego, że nie podałem initialValue, więc do czasu odpowiedzi users() zwraca undefined i pętla @for nie miałaby po czym chodzić. Składnię @if i @for wraz z obowiązkowym track znasz z modułu o komponentach, a track user.id mówi Angularowi, po czym rozpoznawać ten sam wiersz między odświeżeniami. Zwróć teraz uwagę na to, czego toSignal() nie zrobiło, bo tu rodzi się najwięcej złudzeń. Nie wysłało zapytania - zrobił to HttpClient. Nie ponowiło go po błędzie, nie odczekało chwili, nie zapamiętało wyniku na później i nie umie zapytać drugi raz. Sygnalista przy bramie tylko przekłada meldunki na dym, a tego jednego meldunku nikt więcej nie przyniesie.

Cztery kroki mostu

Zatrzymajmy się na chwilę i ułóżmy całą drogę po kolei, bo to jeden z tych ciągów, które warto umieć wyrecytować. Najpierw powstaje strumień, na przykład interval(1000). Potem toSignal() zamienia go na sygnał. Potem odczytujesz wartość przez wywołanie tego sygnału nawiasami - timerSignal(), jeśli nazwiesz pole timerem, albo ticks() w naszym dzwonie. A na końcu effect() z wcześniejszej lekcji tego modułu reaguje na każdą jego zmianę, bo śledzi każdy sygnał odczytany w swoim wnętrzu.

Ostatni krok dopiszmy do komponentu z dzwonem. Cały efekt to jedno wywołanie w konstruktorze, dokładnie w tym samym miejscu, w którym stawiałeś efekty w poprzednich lekcjach.

constructor() {
  effect(() => {
    console.log('Uderzenie numer', this.ticks());
  });
}

Od tej chwili każde uderzenie dzwonu trafia do konsoli. Najważniejsze jest to, czego w tym efekcie nie ma: ani słowa o strumieniu, ani subscribe(), ani interval, ani nawet importu z RxJS. Efekt widzi zwykły sygnał i nie ma pojęcia, że po drugiej stronie mostu biegnie goniec. Dokładnie o to chodzi w całym tym mostku: gdy strumień stanie się sygnałem, wszystko, czego nauczyłeś się w tym module - odczyt nawiasami, wartości pochodne z computed(), efekty - działa na nim bez jednej zmiany.

toObservable - goniec wysłany z wieży

Most bywa potrzebny także w drugą stronę i to nie jest sztuka dla sztuki. Wyobraź sobie okno wyszukiwania wojowników w archiwum Akademii. Uczeń wpisuje literę po literze, a Ty chcesz odczekać trzysta milisekund od ostatniego znaku, zanim ruszysz do serwera, i spróbować jeszcze raz, jeśli połączenie padnie. Sygnał tego nie zrobi. Sygnał umie odpowiedzieć, co jest teraz - nie umie czekać, odmierzać czasu ani ponawiać, bo nie zna pojęcia "później". Umieją to operatory RxJS, o których usłyszysz u zwiadowców ninja. Żeby jednak móc ich użyć, musisz najpierw zamienić wieżę z powrotem w gońca.

Robi to toObservable(), druga funkcja z tego samego pakietu @angular/core/rxjs-interop. Przyjmuje sygnał i zwraca strumień, który wysyła kolejne wartości tego sygnału.

import { Component, signal } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-search-box',
  standalone: true,
  template: '<input (input)="query.set($any($event.target).value)">'
})
export class SearchBoxComponent {
  query = signal('');

  query$ = toObservable(this.query);
}

Masz teraz dwa uchwyty do tej samej rzeczy: query dla szablonu i query$ dla świata strumieni. Nie zmieniło się przy tym nic w samym sygnale - query nadal jest zapisywalny, nadal odczytujesz go nawiasami i nadal to on jest źródłem prawdy. Zapamiętaj parę tych dwóch nazw, bo mylenie ich to najczęstsza pomyłka w tym temacie: toSignal() prowadzi ze strumienia do sygnału, toObservable() z sygnału do strumienia. Kompilator jest tu bezlitosny i to dobra wiadomość. Sprawdziłem oba błędne użycia: podanie sygnału do toSignal() kończy się komunikatem, że WritableSignal nie pasuje do parametru typu Observable, a podanie strumienia do toObservable() - że Observable nie pasuje do parametru typu Signal. Żadna z tych funkcji nie służy natomiast do synchronizowania sygnałów między komponentami, bo od tego jest wspólny serwis z modułu o gildiach, ani do debugowania czegokolwiek.

Jednego tu nie zmyślę, bo nie da się tego uruchomić poza pełną aplikacją: nie uruchamiałem tego strumienia w konsoli. Z dokumentacji typów wynika natomiast, że toObservable() przekazuje wartości przez efekt i dlatego wymaga kontekstu wstrzykiwania, tak samo jak toSignal(). Praktyczny wniosek jest taki, że strumień melduje wartość ustaloną, a nie każdy pojedynczy zapis - jeśli w jednej chwili ustawisz sygnał trzykrotnie, po drugiej stronie zobaczysz stan końcowy.

Wieża czy goniec - jak wybierać

Skoro oba światy dają się połączyć w jedną linijkę, zostaje pytanie najważniejsze: po co w ogóle sięgać po strumień, skoro sygnały są prostsze? Mistrz Ango-san radzi w tej sprawie zawsze tak samo i polecam Ci tę kolejność jako gotowy proces decyzyjny.

Pytanie pierwsze: czy to wartość synchroniczna i prosta, taka, o którą można zapytać "ile teraz"? Poziom wojownika, zaznaczony wiersz, znacznik otwartego panelu. Wtedy bierzesz signal() i na tym koniec. Pytanie drugie: czy ta wartość jest pochodną innych sygnałów, czyli daje się z nich policzyć? Wtedy bierzesz computed() i nigdy więcej o nią nie dbasz. Pytanie trzecie: czy to strumień asynchroniczny, w którym istotny jest czas i kolejność - odpowiedzi z serwera, wiadomości z gniazda WebSocket, ciąg zdarzeń z klawiatury? Tu zaczyna się teren RxJS. Pytanie czwarte, domykające: czy masz już gotowy strumień, a potrzebujesz go w szablonie jako sygnału? Wtedy stawiasz toSignal() i wracasz do świata wież.

Sprawdźmy to na czterech konkretnych zadaniach, bo dopiero wtedy reguła siada w głowie. Przechowywanie prostego stanu wczytywania i błędu to robota dla sygnału - to dwie wartości, o które pytasz "jak jest teraz", i żaden czas w nich nie występuje. Synchroniczna wartość licznika w komponencie to również sygnał, i to podręcznikowy: liczba istnieje w tej chwili i sama z siebie się nie zmieni. Proste obliczenie pochodne z dwóch wartości to computed(), bo Angular przeliczy je sam po każdej zmianie składników. Ale strumień zdarzeń z odczekaniem po ostatnim znaku i ponowieniem po błędzie - czyli nasze okno wyszukiwania - to zadanie, którego sygnał nie wykona. Tu potrzebne są narzędzia operujące na czasie i na kolejności zdarzeń, a takie ma tylko RxJS. Nazwiska tych narzędzi poznasz w następnym module; dziś zapamiętaj samo kryterium: czas i kolejność to teren strumieni, stan tu i teraz to teren sygnałów.

linkedSignal - tablica, którą wolno poprawić kredą

Angular 19 dołożył do arsenału dwa nowe narzędzia i oba rozwiązują kłopoty, które w tej lekcji już zdążyły się pojawić. Pierwszy z nich wygląda tak. Na tablicy w sali narad wisi nazwa oddziału wybranego do wymarszu. Domyślnie jest to pierwszy oddział z listy, ale dowódca ma prawo skreślić go i wpisać kredą inny. Kiedy jednak przychodzi nowy spis oddziałów z zamku, tablicę trzeba przepisać od zera - stary wybór mógł już nie istnieć.

Żadne z narzędzi, które znasz, tego nie robi. Wartość pochodna z computed() sama się przelicza, ale jest tylko do odczytu i dowódca nic na niej nie poprawi - sprawdziłem to kompilatorem, wywołanie set() na takiej wartości kończy się błędem, że taka właściwość nie istnieje. Zwykły sygnał z signal() przeciwnie: pozwala pisać, ale o nowym spisie nigdy się nie dowie i będzie trzymał nazwę rozwiązanego oddziału. Potrzebujesz czegoś pośrodku i to jest właśnie linkedSignal() - sygnał zapisywalny, który jednocześnie resetuje się sam, gdy zmieni się jego źródło. Podajesz mu funkcję liczącą wartość początkową, dokładnie tak jak przy computed().

import { signal, linkedSignal } from '@angular/core';

const squads = signal(['Hikari', 'Kaze', 'Yama']);

const selected = linkedSignal(() => squads()[0]);

Uruchomiłem ten kod: pierwszy odczyt selected() zwraca napis "Hikari", czyli pierwszy oddział ze spisu. Na razie wygląda to jak zwykła wartość pochodna i nie ma w tym nic dziwnego, bo w tym momencie zachowuje się identycznie. Różnica pokaże się dopiero przy pisaniu i przy podmianie spisu.

selected.set('Yama');

squads.set(['Tora', 'Ryu']);

Oba wywołania sprawdziłem na uruchomionym kodzie. Po pierwszym selected() zwraca "Yama" - a więc zapis się udał, czego przy computed() nawet nie dałoby się skompilować. Po drugim selected() zwraca "Tora", czyli pierwszy oddział z nowego spisu. Nikt tego nie ustawiał: zmiana źródła sama unieważniła kredowy dopisek i tablica została przepisana od nowa. I znów najważniejsze jest to, co się nie zmieniło - sygnał squads nie wie o istnieniu selected i nie ma z nim żadnej umowy, a po resecie selected pozostaje w pełni zapisywalny i dowódca może zaraz wpisać "Ryu".

Trzy inne opisy linkedSignal() brzmią wiarygodnie i wszystkie trzy są fałszywe. Nie jest to sygnał stały, którego wartości nie da się zmienić - właśnie ustawiłeś go dwa razy, a metody set i update są na nim obecne, co sprawdziłem na uruchomionym obiekcie. Nie łączy też dwóch komponentów przez wspólny serwis; do dzielenia stanu między komponentami służy serwis z modułu o gildiach, a linkedSignal() żyje sobie w jednym pliku i o żadnych komponentach nie wie. I nie konwertuje strumieni na sygnały - pochodzi z pakietu @angular/core, nie z mostka do RxJS, i o istnieniu Observable nie ma pojęcia. Od zamiany strumienia w sygnał jest toSignal() i tylko ono.

resource() - wyprawa po zwój

Drugie narzędzie z Angulara 19 wraca do sprawy, którą zostawiliśmy niedokończoną przy liście użytkowników. Tamten most działał, dopóki dane pobierało się raz. A co, jeśli komponent pokazuje kartę jednego wojownika, numer wojownika przychodzi z zewnątrz przez input.required() z poprzedniej lekcji, i ten numer się zmienia? Strumień z http.get() przyniósł swoją jedną odpowiedź i zamilkł na zawsze. Trzeba by zbudować nowy strumień dla nowego numeru i porzucić poprzedni w pół drogi - da się to zrobić operatorami RxJS, o których usłyszysz w następnym module, ale trzeba wiedzieć jak.

Angular 19 daje na to odpowiedź pomyślaną od początku pod sygnały: funkcję resource(). Nazwij ją sobie wyprawą po zwój do odległej biblioteki. Podajesz jej obiekt z dwoma polami. Pole request to funkcja mówiąca, po co się wybieramy - odczytujesz w niej sygnały, a Angular pilnuje, żeby zmiana któregokolwiek z nich wysłała wyprawę na nowo. Pole loader to funkcja mówiąca, jak zdobyć dane; jest asynchroniczna i dostaje w argumencie aktualne request. W środku użyję przeglądarkowej funkcji fetch, a odpowiedź zamienię na obiekt metodą json(). Karta wojownika to w naszym kodzie interfejs Samurai z polami id oraz name. Poniżej widzisz samo pole klasy - reszta komponentu, wraz z id = input.required<number>(), wygląda tak jak w poprzedniej lekcji.

samurai = resource({
  request: () => this.id(),
  loader: async ({ request: id }) => {
    const response = await fetch(`/api/samurai/${id}`);
    return response.json() as Promise<Samurai>;
  }
});

Zapis { request: id } w nagłówku funkcji to wyciągnięcie pola request z argumentu i nazwanie go krócej id. Cały ten kod skompilowałem i przechodzi bez zastrzeżeń. Nie uruchamiałem go natomiast, bo resource() potrzebuje działającej aplikacji Angulara - próba wywołania go w gołym Node kończy się błędem o braku wewnętrznego dostawcy. Dlatego zamiast wymyślać wyniki, powiem wprost, co się dzieje: gdy this.id() zmieni wartość, Angular wywołuje loader jeszcze raz dla nowego numeru, a poprzednią, niedokończoną wyprawę przerywa. Nie zmieniło się przy tym nic w sposobie pobierania danych - w środku nadal siedzi zwykły fetch, który napisałeś sam.

Wynik wyprawy odczytujesz sygnałami i to jest cała nagroda za ten zapis. Pole value() zwraca dane, gdy już są. Pole error() zwraca błąd, jeśli wyprawa się nie powiodła. Pole isLoading() zwraca prawdę albo fałsz i najwygodniej używać go w szablonie. A pole status() melduje fazę całej wyprawy - to o nim za chwilę powiemy więcej.

@if (samurai.isLoading()) {
  <p>Goniec w drodze...</p>
} @else if (samurai.error()) {
  <p>Brak meldunku</p>
} @else {
  <p>{{ samurai.value()?.name }}</p>
}

Ten szablon obsługuje wszystkie trzy zakończenia wyprawy i nie potrzebuje ani jednego pola pomocniczego w klasie. Pytajnik w zapisie value()?.name jest konieczny, bo dopóki dane nie dotarły, value() zwraca undefined - to ta sama ostrożność, którą wcześniej wymusił na Tobie toSignal() bez wartości początkowej.

Zostaje obiecana faza, czyli status(). Melduje ona, na jakim etapie jest wyprawa: czy dane wciąż się ładują, czy przyszły i mamy sukces, czy skończyło się błędem. Warto wiedzieć również, czego tam nie ma: nie istnieje żadna faza wstrzymania, żadne "paused". Wyprawy po zwój nie da się zatrzymać w połowie drogi i kazać jej czekać na sygnał - można ją najwyżej powtórzyć metodą reload() albo przerwać razem z komponentem. Uczciwie dodam, że w Angularze 19 całe resource() jest oznaczone jako eksperymentalne, a dokładny zapis nazw tych faz był między wydaniami doprecyzowywany. W aktualnym Angularze status() zwraca jedną z wartości: 'idle', 'loading', 'reloading', 'resolved' (sukces), 'error' albo 'local' (wartość ustawiona ręcznie przez set()) - i ani śladu pauzy.

Pamiętaj z tej lekcji jedno: wieża odpowiada, jak jest teraz, goniec przynosi wieści rozłożone w czasie, a toSignal() i toObservable() to jedyne dwa mosty między nimi - jeden ze szlaku na mury, drugi z murów na szlak.

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Do czego służy funkcja toSignal() z @angular/core/rxjs-interop?

  2. 2. Do którego zadania lepiej nadaje się RxJS niż Signals?

To 2 z 4 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Układanie w poziomie

    Ułóż w jednej linii import funkcji toSignal z pakietu rxjs-interop.

  • Edytor kodu

    Uzupełnij komponent konwertujący Observable z HTTP na Signal.

  • Układanie w pionie

    Uporządkuj proces połączenia strumienia RxJS z signalem w komponencie.

  • Klikanie w kolejności

    Ułóż wywołanie konwertujące obserwowalny strumień timer$ na signal.

  • Układanie w pionie

    Uporządkuj proces decyzyjny przy wyborze narzędzia reaktywności dla stanu UI.

Przydatne artykuły