HttpClient — pobieranie danych z serwera
Do tej pory dane były wpisane w kodzie. W prawdziwej aplikacji przychodzą z serwera — z opóźnieniem, a czasem wcale. Pobierzesz listę wydarzeń z pliku JSON, obsłużysz ładowanie i błąd, a na koniec przeniesiesz pobieranie do serwisu.
Czego się dziś nauczysz
- Przygotujesz plik JSON w folderze
publici otworzysz go w przeglądarce. - Pobierzesz tablicę obiektów przez
HttpClienti zapiszesz ją w sygnale. - Obsłużysz trzy stany: ładowanie, dane i błąd — sygnałami i blokami
@if. - Podejrzysz zapytanie w zakładce Network narzędzi programisty.
- Przeniesiesz pobieranie do serwisu, z którego korzysta kilka komponentów.
Przygotowanie: lekcja 07 (folder public), lekcje 09–13 (sygnały i bloki sterujące), lekcja 30 (interfejs), lekcja 33 (serwis i inject). Przewidywany czas: 50 min z zadaniami.
Dane przychodzą z opóźnieniem
Pobranie danych z sieci trwa — czasem 50 milisekund, czasem 5 sekund, a czasem kończy się błędem. Program nie może na to czekać, zatrzymując całą stronę. Dlatego HttpClient działa tak: wysyłasz zapytanie, podajesz funkcję „co zrobić, gdy przyjdzie odpowiedź” i program idzie dalej.
| Krok | Kod | Uwagi |
|---|---|---|
| 1. Plik z danymi | public/dane/wydarzenia.json | w przeglądarce pod adresem /dane/wydarzenia.json — jak obrazki z lekcji 07 |
| 2. HttpClient w klasie | private readonly http = inject(HttpClient); | import z @angular/common/http |
| 3. Zapytanie | this.http.get<Wydarzenie[]>('/dane/wydarzenia.json') | typ mówi, czego się spodziewamy |
| 4. Odbiór odpowiedzi | .subscribe(dane => this.wydarzenia.set(dane)) | bez subscribe zapytanie nie zostanie wysłane |
Konfiguracja w Angularze 22
W Angularze 22 HttpClient jest dostępny od razu — nie trzeba niczego dopisywać w app.config.ts. W starszych projektach i poradnikach zobaczysz w providers wywołanie provideHttpClient(); dziś dopisuje się je tylko wtedy, gdy trzeba zmienić ustawienia klienta HTTP.
[
{ "id": 1, "tytul": "Turniej retro", "sala": "12", "miejsca": 16 },
{ "id": 2, "tytul": "Noc robotów", "sala": "7", "miejsca": 0 },
{ "id": 3, "tytul": "Warsztat druku 3D", "sala": "14", "miejsca": 3 },
{ "id": 4, "tytul": "Liga planszówek", "sala": "Biblioteka", "miejsca": 10 }
]
JSON wygląda prawie jak tablica obiektów w TypeScripcie, ale ma ostrzejsze reguły: nazwy pól i teksty w cudzysłowach (nie apostrofach), bez przecinka po ostatnim elemencie, bez komentarzy. Po zapisaniu pliku otwórz w przeglądarce localhost:4200/dane/wydarzenia.json — jeśli widzisz dane, adres jest dobry.
Pierwsze pobranie danych
Interfejs opisuje jeden obiekt z pliku JSON. Komponent przy starcie pobiera listę i wyświetla ją w tabeli.
export interface Wydarzenie {
id: number;
tytul: string;
sala: string;
miejsca: number;
}
import { Component, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Wydarzenie } from './wydarzenie';
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
private readonly http = inject(HttpClient);
readonly wydarzenia = signal<Wydarzenie[]>([]);
constructor() {
this.http.get<Wydarzenie[]>('/dane/wydarzenia.json').subscribe(dane => {
this.wydarzenia.set(dane);
});
}
}
<div class="p-3">
<table class="table" style="max-width: 36rem">
<thead><tr><th>Wydarzenie</th><th>Sala</th><th>Wolne miejsca</th></tr></thead>
<tbody>
@for (w of wydarzenia(); track w.id) {
<tr><td>{{ w.tytul }}</td><td>{{ w.sala }}</td><td>{{ w.miejsca }}</td></tr>
}
</tbody>
</table>
</div>
Wydarzenie Sala Wolne miejsca
Turniej retro 12 16
Noc robotów 7 0
Warsztat druku 3D 14 3
Liga planszówek Biblioteka 10
constructor() to metoda wywoływana raz, przy tworzeniu komponentu — dobre miejsce na start pobierania. subscribe(dane => …) przyjmuje funkcję, którą Angular wywoła później, gdy odpowiedź dotrze. Zmień w pliku JSON dowolny tytuł i odśwież stronę — zmiana pojawi się bez zmieniania kodu.
Zakładka Network
F12 → Network (Sieć) i odśwież stronę: na liście jest zapytanie wydarzenia.json ze statusem 200. Kliknij je i zajrzyj w zakładkę Response — to dokładnie zawartość pliku. Tak sprawdzisz każde zapytanie, które „nie działa”.
Zatrzymaj się
Dopisz pod tabelą {{ wydarzenia().length }} i odśwież stronę kilka razy. Czy przez chwilę widać 0? Dlaczego ta chwila może być bardzo krótka albo bardzo długa?
Ładowanie, dane, błąd
Ten sam komponent z pełną obsługą stanów. subscribe dostaje obiekt z dwiema funkcjami: next (przyszły dane) i error (coś się nie udało). Dwa przyciski pobierają dane z dobrego i złego adresu, żeby zobaczyć oba zakończenia.
import { Component, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Wydarzenie } from './wydarzenie';
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
private readonly http = inject(HttpClient);
readonly ladowanie = signal(false);
readonly blad = signal('');
readonly wydarzenia = signal<Wydarzenie[]>([]);
pobierz(adres: string): void {
this.ladowanie.set(true);
this.blad.set('');
this.wydarzenia.set([]);
this.http.get<Wydarzenie[]>(adres).subscribe({
next: dane => {
this.wydarzenia.set(dane);
this.ladowanie.set(false);
},
error: () => {
this.blad.set('Nie udało się pobrać danych z adresu ' + adres);
this.ladowanie.set(false);
}
});
}
}
<div class="p-3">
<div style="max-width: 36rem">
<button class="btn btn-primary me-2" (click)="pobierz('/dane/wydarzenia.json')">Pobierz</button>
<button class="btn btn-outline-danger" (click)="pobierz('/dane/brak-takiego-pliku.json')">Pobierz ze złego adresu</button>
<div class="mt-3">
@if (ladowanie()) {
<div class="spinner-border spinner-border-sm"></div> Ładowanie…
} @else if (blad()) {
<div class="alert alert-danger">{{ blad() }}</div>
} @else {
<ul class="list-group">
@for (w of wydarzenia(); track w.id) {
<li class="list-group-item">{{ w.tytul }} — sala {{ w.sala }}</li>
} @empty {
<li class="list-group-item text-secondary">Kliknij „Pobierz”.</li>
}
</ul>
}
</div>
</div>
</div>
[Pobierz] [Pobierz ze złego adresu]
Nie udało się pobrać danych z adresu /dane/brak-takiego-pliku.json (czerwony alert)
Plik wydarzenie.ts jest taki jak w przykładzie 1. Trzy sygnały opisują stan, a łańcuch @if … @else if … @else pokazuje dokładnie jeden z nich. W zakładce Network złe zapytanie ma status 404. Na szybkim komputerze „Ładowanie…” mignie tak krótko, że go nie zobaczysz — tym zajmuje się przykład 3.
Zatrzymaj się
W obu funkcjach — next i error — jest this.ladowanie.set(false). Co by się stało, gdybyś zapomniał o nim w error?
Symulacja wolnego połączenia
Żeby zobaczyć stan ładowania, spowalniamy odpowiedź operatorem delay z biblioteki RxJS, na której opiera się HttpClient. .pipe(delay(1500)) wstrzymuje odpowiedź o półtorej sekundy. Przycisk jest zablokowany w trakcie ładowania, żeby nie wysyłać zapytań seriami.
import { Component, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { delay } from 'rxjs';
import { Wydarzenie } from './wydarzenie';
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
private readonly http = inject(HttpClient);
readonly ladowanie = signal(false);
readonly blad = signal('');
readonly wydarzenia = signal<Wydarzenie[]>([]);
readonly ostatnio = signal<Date | null>(null);
constructor() {
this.odswiez();
}
odswiez(): void {
this.ladowanie.set(true);
this.blad.set('');
this.http.get<Wydarzenie[]>('/dane/wydarzenia.json')
.pipe(delay(1500))
.subscribe({
next: dane => {
this.wydarzenia.set(dane);
this.ostatnio.set(new Date());
this.ladowanie.set(false);
},
error: () => {
this.blad.set('Brak połączenia z serwerem.');
this.ladowanie.set(false);
}
});
}
}
<div class="card m-3" style="max-width: 32rem">
<div class="card-header d-flex justify-content-between align-items-center">
Wydarzenia
<button class="btn btn-sm btn-outline-primary" [disabled]="ladowanie()" (click)="odswiez()">
@if (ladowanie()) { <span class="spinner-border spinner-border-sm"></span> } Odśwież
</button>
</div>
<ul class="list-group list-group-flush" [class.opacity-50]="ladowanie()">
@for (w of wydarzenia(); track w.id) {
<li class="list-group-item">{{ w.tytul }}</li>
}
</ul>
@if (blad()) { <div class="alert alert-danger m-2">{{ blad() }}</div> }
@if (ostatnio(); as o) {
<div class="card-footer small text-secondary">Odświeżono o {{ o.toLocaleTimeString() }}</div>
}
</div>
Wydarzenia [◌ Odśwież] (zablokowany)
──────────────────────────────────────────
Wydarzenia [Odśwież]
Turniej retro
Noc robotów
Warsztat druku 3D
Liga planszówek
Odświeżono o 18:42:07
signal<Date | null>(null) to sygnał, który może być datą albo null — na starcie nie ma jeszcze daty odświeżenia. „Odśwież” wygasza listę (opacity-50) na czas ładowania, ale jej nie czyści — użytkownik widzi stare dane, dopóki nie przyjdą nowe. Wolne łącze możesz też ustawić bez zmiany kodu: F12 → Network → No throttling → Slow 4G. Przed oddaniem projektu usuń delay.
Zatrzymaj się
W przykładzie 2 lista była czyszczona przed pobraniem, tu nie. Które zachowanie jest wygodniejsze dla użytkownika i kiedy?
Pobieranie w serwisie
Dane z serwera zwykle potrzebuje kilka komponentów. Pobieranie przenosimy do serwisu WydarzeniaSerwis (lekcja 33): ma sygnały stanu i metodę wczytaj, która nie pobiera drugi raz, jeśli dane już są albo właśnie się pobierają. Licznik i lista tylko z niego czytają.
import { Service, computed, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Wydarzenie } from './wydarzenie';
@Service()
export class WydarzeniaSerwis {
private readonly http = inject(HttpClient);
readonly lista = signal<Wydarzenie[]>([]);
readonly ladowanie = signal(false);
readonly blad = signal('');
readonly wolneRazem = computed(() => {
let suma = 0;
for (const w of this.lista()) {
suma += w.miejsca;
}
return suma;
});
wczytaj(): void {
// dane już są albo właśnie się pobierają — nie wysyłamy drugiego zapytania
if (this.lista().length > 0 || this.ladowanie()) {
return;
}
this.ladowanie.set(true);
this.http.get<Wydarzenie[]>('/dane/wydarzenia.json').subscribe({
next: dane => {
this.lista.set(dane);
this.ladowanie.set(false);
},
error: () => {
this.blad.set('Nie udało się pobrać wydarzeń.');
this.ladowanie.set(false);
}
});
}
}
import { Component, inject } from '@angular/core';
import { WydarzeniaSerwis } from './wydarzenia-serwis';
@Component({
selector: 'app-licznik',
imports: [],
template: `
<div class="alert alert-info">
Wydarzeń: {{ dane.lista().length }}, wolnych miejsc łącznie: {{ dane.wolneRazem() }}
</div>
`
})
export class Licznik {
readonly dane = inject(WydarzeniaSerwis);
}
import { Component, inject } from '@angular/core';
import { WydarzeniaSerwis } from './wydarzenia-serwis';
@Component({
selector: 'app-lista',
imports: [],
template: `
@if (dane.ladowanie()) {
<p>Ładowanie…</p>
} @else if (dane.blad()) {
<div class="alert alert-danger">{{ dane.blad() }}</div>
} @else {
<ul class="list-group">
@for (w of dane.lista(); track w.id) {
<li class="list-group-item d-flex justify-content-between">
{{ w.tytul }}
<span class="badge" [class.text-bg-success]="w.miejsca > 0" [class.text-bg-danger]="w.miejsca === 0">{{ w.miejsca }}</span>
</li>
}
</ul>
}
`
})
export class Lista {
readonly dane = inject(WydarzeniaSerwis);
constructor() {
this.dane.wczytaj();
}
}
import { Component } from '@angular/core';
import { Licznik } from './licznik';
import { Lista } from './lista';
@Component({
selector: 'app-demo',
imports: [Licznik, Lista],
templateUrl: './demo.html'
})
export class Demo {}
<div class="p-3">
<div style="max-width: 34rem">
<app-licznik />
<app-lista />
</div>
</div>
Wydarzeń: 4, wolnych miejsc łącznie: 29
Turniej retro [16]
Noc robotów [0]
Warsztat druku 3D [3]
Liga planszówek [10]
W zakładce Network jest jedno zapytanie, choć z danych korzystają dwa komponenty. Gdyby trzeci i czwarty komponent też wywoływał wczytaj(), zapytanie i tak byłoby jedno — serwis pamięta, że dane już ma albo właśnie je pobiera.
httpResource
W nowszym kodzie zobaczysz też httpResource(() => '/dane/wydarzenia.json') — funkcję, która sama tworzy sygnały wartości, ładowania i błędu. Działa na tej samej zasadzie co nasze trzy sygnały; warto ją poznać, gdy ręczna wersja jest już jasna.
Zatrzymaj się
Licznik nie wywołuje wczytaj(), a mimo to pokazuje dane. Co by pokazał, gdyby na stronie był sam licznik, bez listy?
Zestawienie elementów
| Element | Znaczenie |
|---|---|
HttpClient z @angular/common/http | klient HTTP; pobierany przez inject |
http.get<Typ>(adres) | przygotowuje zapytanie GET (Observable — „obietnica” danych) |
.subscribe(dane => …) | wysyła zapytanie i odbiera dane |
.subscribe({ next: …, error: … }) | osobna obsługa sukcesu i błędu |
.pipe(delay(1500)), delay z rxjs | sztuczne opóźnienie odpowiedzi |
constructor() { … } | kod uruchamiany raz przy tworzeniu komponentu lub serwisu |
signal<Date | null>(null) | sygnał, który może nie mieć jeszcze wartości |
new Date().toLocaleTimeString() | bieżąca godzina jako tekst |
spinner-border spinner-border-sm | wskaźnik ładowania Bootstrapa |
Zanim utkniesz
| Zapis | Problem |
|---|---|
this.http.get(…) bez subscribe | Nic się nie dzieje — w zakładce Network nie ma żadnego zapytania. Zapytanie wysyła dopiero subscribe. |
const dane = this.http.get(…); this.lista.set(dane); | TS2345: Argument of type 'Observable<…>’ is not assignable to parameter of type '…[]’. get nie zwraca danych, tylko „obietnicę”; dane przychodzą w funkcji z subscribe. |
Adres public/dane/… albo dane/… bez ukośnika | Błąd 404 albo adres liczony względem bieżącej strony. Folder public nie występuje w adresie; zaczynaj od ukośnika: /dane/wydarzenia.json. |
| Przecinek po ostatnim elemencie, apostrofy w JSON | Status 200, a mimo to wywołuje się error: Http failure during parsing. Plik nie jest poprawnym JSON-em. |
Brak funkcji error | Przy błędzie użytkownik widzi wieczne „Ładowanie…”. Zawsze obsłuż oba zakończenia. |
Pola interfejsu inne niż w pliku JSON (nazwa zamiast tytul) | Brak błędu kompilacji — TypeScript wierzy typowi z get<…>. Na stronie puste komórki. Sprawdź nazwy w Response. |
Zadania
Utwórz public/dane/uczniowie.json z tablicą pięciu uczniów (imie, klasa, srednia) i wyświetl ich w tabeli.
Kryterium sukcesu: plik otwiera się w przeglądarce pod /dane/uczniowie.json; zmiana w pliku po odświeżeniu strony zmienia tabelę.
Podpowiedź
Przykład 1 z innym interfejsem i adresem.
Plik menu.json z daniami (nazwa, cena, wege — true/false). Wyświetl je jako listę z ceną w złotych; dania wegetariańskie mają zieloną etykietę „wege”.
Kryterium sukcesu: lista zgadza się z plikiem; etykieta tylko przy daniach z "wege": true.
Podpowiedź
W JSON wartości logiczne piszesz bez cudzysłowów: true, false. Cena: pipe currency z lekcji 16 albo toFixed(2).
Plik plan.json z planem na pięć dni (tablica obiektów: dzien i tablica przedmiotów). Wyświetl go jako karty, z obsługą stanu ładowania i błędu oraz przyciskiem „Odśwież”.
Kryterium sukcesu: widoczne są wszystkie trzy stany (ładowanie z delay, dane, błąd po zmianie adresu na zły).
Podpowiedź
Przykład 3. Dla zagnieżdżonych tablic potrzebujesz dwóch pętli @for, jednej w drugiej.
Pobierz wydarzenia.json, a nad listą dodaj pole wyszukiwania z [(ngModel)] i checkbox „tylko z wolnymi miejscami”. Lista pokazuje tylko pasujące wydarzenia.
Kryterium sukcesu: filtrowanie działa po każdym znaku; dane pobierane są raz (jedno zapytanie w Network).
Podpowiedź
Pobrane dane w jednym sygnale, filtr w computed z filter i toLowerCase().includes(…).
Połącz lekcje 35 i 36: serwis pobiera listę wydarzeń z JSON-a, lista po lewej linkuje do wydarzenie/:id, a szczegóły szukają wydarzenia w danych serwisu. Wejście bezpośrednio na adres szczegółów (F5) też musi działać.
Kryterium sukcesu: lista i szczegóły korzystają z tych samych danych; po odświeżeniu strony na szczegółach dane się wczytują i karta się pokazuje.
Podpowiedź
Szczegóły też wywołują wczytaj(), a computed z find czyta serwis.lista() — przeliczy się, gdy dane dotrą.
Plik quiz.json z pięcioma pytaniami (pytanie, odpowiedzi — tablica, poprawna — numer). Aplikacja pobiera pytania, pokazuje je po jednym z odpowiedziami jako przyciskami, liczy punkty i na końcu pokazuje wynik oraz przycisk „Od nowa”.
Kryterium sukcesu: pytania pochodzą wyłącznie z pliku; dopisanie szóstego pytania do JSON-a wydłuża quiz bez zmiany kodu; jest obsługa ładowania i błędu.
Podpowiedź
Sygnały: pytania, nr, punkty; bieżące pytanie to computed(() => this.pytania()[this.nr()]).
Co trzeba zapamiętać
- Pliki z
publicsą dostępne pod adresem od ukośnika:/dane/plik.json. inject(HttpClient), potemhttp.get<Typ>(adres).subscribe(…)— bezsubscribezapytanie nie jest wysyłane.- Ładowanie, dane i błąd to trzy sygnały;
subscribe({ next, error })obsługuje oba zakończenia. - Zakładka Network pokazuje każde zapytanie, jego status i odpowiedź.
- Pobieranie umieszczaj w serwisie, żeby kilka komponentów korzystało z jednego zapytania.
Sprawdź się: w zakładce Network zapytanie ma status 200, a aplikacja pokazuje komunikat o błędzie. Co sprawdzisz w pierwszej kolejności?
Dokumentacja: angular.dev — Making HTTP requests.