Server State'i Yönetmek: resource, httpResource, chain() ve Mutation Sonrası Invalidation
Angular 22'de server state, loading, error, reloading ve local durumlarını kendisi taşıyan resource ailesiyle yönetilir: okuma httpResource ile, yazma HttpClient ile yapılır, mutation'dan sonra resource reload() ile tazelenir. Bookstore'un kitap sayfasında chain() ile bağımlı resource'ları, optimistic yorum eklemeyi ve hatada geri almayı, iki component'in aynı listeyi ayrı çekmesini, NgRx 22'nin withPreviousValueOnLoading'ini ve error'da value() okumayı gerçek çıktılarla gösteriyoruz.
Sürüm notu: Bu yazıdaki örnekler Angular 22.2, NgRx 22.0.1 (@ngrx/signals), TypeScript 6.0 ve Node.js 24 ile denenmiştir (Angular 22.2 projeleri TypeScript 6.0 kullanıyor); tarayıcı davranışı Playwright 1.65 ile headless Chromium'da, snapshot'taki sahte Bookstore API'sine karşı gözlenmiştir. SSR (Server-Side Rendering; sayfanın ilk HTML'inin sunucuda üretilmesi) ve TanStack Query ile ilgili bölümler çalıştırılmadı, dokümantasyondan anlatılıyor.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) Angular istemcisi bookstore-web'e bir kitap detay sayfası ekledik. Sayfada kitabın yorumları bir httpResource'ta duruyor ve kullanıcı yeni bir yorum ekleyebiliyor. Yorum ekleyen method yorumu sunucunun cevabını beklemeden listeye koyuyor:
async addReview(): Promise<void> {
const draft = { author: 'Ada', rating: 5, text: this.text() };
const before = this.reviews.value();
this.saving.set(true);
this.saveError.set(null);
// Optimistic update: yorumu hemen göster; resource'un status'ü 'local' olur
this.reviews.update((list) => [...list, { ...draft, id: -1, isbn: this.isbn() }]);
try {
await firstValueFrom(this.#http.post<Review>(`${apiUrl}/books/${this.isbn()}/reviews`, draft));
this.text.set('');
// Source of truth yine sunucu: listeyi gerçek id ile yeniden çek
this.reviews.reload();
} catch {
// Optimistic update'ten önceki listeye geri dön
this.reviews.set(before);
this.saveError.set('Could not save your review. Please try again.');
} finally {
this.saving.set(false);
}
}
Dune'un iki yorumu var. Kullanıcı "Read it twice." yazıp gönderiyor, ama sunucu bu POST'a 503 dönüyor. Butona basıldıktan hemen sonra listede kaç yorum görünür? Hata geldikten sonra kaç? Resource'un status()'ü hangi değerlerden geçer: loading, reloading, local, error? Tahminini yaz; gerçek çıktı "Mutation Sonrası" bölümünde.
Serinin ilk yazısında server state'i "sahibi API olan, istemcideki kopyası bayatlayan veri" diye tanımladık. Bu yazı, o veriyi Angular 22'de stable olan resource ailesiyle yönetmeyi anlatıyor: status'ler, hasValue(), parametreli httpResource, chain(), optimistic update ve geri alma, reload(), aynı verinin iki component tarafından iki kez çekilmesi ve NgRx 22'nin resource extension'ları. resource ve httpResource'un tanıtımı Angular serisinin "Angular 19: Varsayılan Standalone, linkedSignal ve resource() ile Veri Yükleme" ve "Angular 20: Stable Signal API'leri, httpResource ve Yeni Dosya Adlandırma" yazılarında; burada mimari kullanımına bakıyoruz.
Server State'in Kendine Özgü Problemleri
Sepet gibi bir client state'in tek bir doğru değeri var ve o değer bellekte. Server state'te bellekteki değer her zaman bir kopya ve kopyanın yaşam döngüsü, client state'te karşılaşmadığın beş soruyu getiriyor:
- Yüklenirken ne gösterilecek? İlk yükleme ile "elimde eski veri var, yenisini bekliyorum" durumu farklı şeyler.
- İstek başarısız olursa ne gösterilecek ve sayfanın geri kalanı çalışmaya devam edecek mi?
- Kopya ne zaman bayatladı? Başka bir müşteri sipariş verince stok değişiyor; ilk yazıdaki global store örneği tam olarak bunu kaçırıyordu.
- Aynı veriyi iki component istiyorsa iki istek mi gidecek?
- Kullanıcı veriyi değiştirince (mutation) ekrandaki kopya ne zaman ve nasıl güncellenecek?
Bunları elle bir store'da çözmek mümkün, ama her biri ayrı bir field ve ayrı bir kural demek: isLoading, error, lastFetchedAt, istek iptali... Angular'ın resource ailesi ilk üçünü doğrudan modelliyor; son ikisi için de yapı taşları veriyor.
httpResource ile Okumak: Status ve hasValue
Kitap detay sayfasının resource'ları:
export class BookDetailsPage {
readonly #http = inject(HttpClient);
// withComponentInputBinding() :isbn route parametresinden doldurur
readonly isbn = input.required<string>();
protected readonly book = httpResource<Book>(() => `${apiUrl}/books/${this.isbn()}`);
protected readonly reviews = httpResource<Review[]>(() => `${apiUrl}/books/${this.isbn()}/reviews`, {
defaultValue: [],
});
// Kitabın kategorisine bağlı: kitabı bekler, onun loading ve error durumunu izler
protected readonly related = httpResource<BookPage>(({ chain }) => {
const category = chain(this.book)?.category;
return category ? { url: `${apiUrl}/books`, params: { category } } : undefined;
});
protected readonly relatedBooks = computed(() =>
this.related.hasValue() ? this.related.value().items.filter((b) => b.isbn !== this.isbn()) : [],
);
protected readonly text = signal('');
protected readonly saving = signal(false);
protected readonly saveError = signal<string | null>(null);
constructor() {
effect(() => console.log(`[book] ${this.book.status()} | [related] ${this.related.status()}`));
effect(() => console.log(`[reviews] ${this.reviews.status()}: ${this.reviews.value().length} reviews`));
}
httpResource'a bir URL ya da request object'i döndüren reaktif bir fonksiyon veriliyor; fonksiyonun okuduğu signal'lar (burada isbn) değişince istek yeniden atılıyor, önceki istek sürüyorsa iptal ediliyor. Resource'un durumu signal'larda: value(), status(), error(), isLoading() ve hasValue(). Constructor'daki iki effect yalnız bu yazı için status'leri konsola yazıyor. Sayfayı açınca:
0.1 s request: GET /categories
0.1 s request: GET /books/9780441172719
0.1 s request: GET /books/9780441172719/reviews
0.1 s console: [book] loading | [related] loading
0.1 s console: [reviews] loading: 0 reviews
0.4 s request: GET /books?category=science-fiction
0.4 s console: [book] resolved | [related] loading
0.4 s console: [reviews] resolved: 2 reviews
0.7 s console: [book] resolved | [related] resolved
--- user adds a review
1.7 s request: POST /books/9780441172719/reviews
1.7 s console: [reviews] local: 3 reviews
screen right after click: 3 reviews
2.0 s request: GET /books/9780441172719/reviews
2.0 s console: [reviews] reloading: 3 reviews
2.3 s console: [reviews] resolved: 3 reviews
screen: Ada (5/5): Still the best world-building I know. | Alan (4/5): Slow start, great second half. | Ada (5/5): Read it twice.
error : -
Bu çıktının ilk dokuz satırı okuma tarafı. Kitap ve yorumlar paralel istendi, ikisi de loading ile başlayıp resolved'a geçti. status()'ün altı değeri var: idle (geçerli bir istek yok), loading (parametre değişti, değer yükleniyor), reloading (aynı parametreyle reload() çağrıldı, eski değer duruyor), resolved, error ve local (değer elle set/update ile yazıldı). Template bu değerleri @if zinciriyle ekrana çeviriyor:
@if (book.hasValue()) {
<h1>{{ book.value().title }}</h1>
<p class="book">{{ book.value().author }} - {{ book.value().price }} TRY</p>
} @else if (book.error()) {
<p class="book-error">Could not load this book.</p>
} @else {
<p>Loading...</p>
}
Sıra önemli: önce hasValue(), sonra error(). Angular dokümantasyonuna göre resource error durumundayken value()'yu okumak hata fırlatıyor; hasValue() hem bu durumu hem de değerin henüz gelmediği durumu ayırıyor ve TypeScript'te tipi undefined'dan arındıran bir type guard olarak çalışıyor. Bu kuralı çiğnemenin bedelini "Sık Düşülen Tuzaklar"da gerçek bir hata mesajıyla göreceğiz.
Yorumların defaultValue: [] seçeneği, liste henüz gelmemişken value()'nun undefined yerine boş array döndürmesini sağlıyor; template'te ayrıca kontrol gerekmiyor. Bu, "yükleniyor" ile "hiç yorum yok" ayrımının önemsiz olduğu bir liste için uygun. Kitabın kendisi için değil: orada "yükleniyor" ile "kitap yok" farklı ekranlar.
chain(): Birbirine Bağlı Resource'lar
"More in this category" listesi kitabın kategorisine bağlı; kategori ancak kitap geldikten sonra biliniyor. related resource'unun fonksiyonu bir chain parametresi alıyor: chain(this.book), kitap resource'u resolved ya da local ise değerini döndürüyor; kitap henüz loading ise related'ı da loading'e, error ise error'a, idle ise idle'a taşıyor ve fonksiyonun geri kalanını çalıştırmıyor. Yukarıdaki çıktıda bu sıra görünüyor: kitap 0,4. saniyede resolved olana kadar related loading'de bekledi ve /books?category=science-fiction isteği ancak o anda gitti.
Fonksiyonun içindeki küçük ayrıntı, Angular dokümantasyonunun da uyardığı bir tuzaktan kaçıyor: kategoriyi bir object'in içine koyup döndürseydim ({ category: undefined }), bu tanımlı bir değer olurdu ve istek category=undefined ile giderdi. Değer yoksa undefined döndürmek resource'u idle'a alıyor. chain yerine this.book.value()'yu doğrudan okumak da çalışırdı, ama kitap hata verdiğinde related idle'da kalırdı; chain yukarıdaki resource'un hatasını aşağıya taşıyor. Bağımlılık yalnız senkron bir türetme ise (ör. kitabın fiyatından indirimli fiyatı hesaplamak), ikinci bir resource değil computed yeterli.
Mutation Sonrası: Optimistic Update, Geri Alma ve reload()
Angular dokümantasyonu httpResource'u POST ya da PUT gibi mutation'lar için kullanmamayı öneriyor; resource okumak için. Yorum ekleme bu yüzden doğrudan HttpClient ile yapılıyor ve resource'un üç yeteneğini kullanıyor: update ile yorumu hemen listeye koymak (optimistic update), hata olursa set ile eski listeye dönmek, başarıdan sonra reload() ile listeyi sunucudan tazelemek.
Yukarıdaki çıktının ikinci yarısı başarılı senaryo. Butona basıldığı an status local ve 3 yorum: kullanıcı yorumunu beklemeden görüyor. POST 0,3 saniye sonra döndü, reload() yeni bir GET attı; bu sırada status reloading ve eski değer (3 yorum) ekranda kaldı, sonra resolved. reload()'un gerekçesi, optimistic yorumun id: -1 ile eklenmiş geçici bir kopya olması; listenin gerçek hâli (gerçek id, sunucunun eklediği alanlar, bu arada başkalarının eklediği yorumlar) sunucuda. Şimdi "Önce Tahmin Et"teki senaryo, sahte API ilk POST'a 503 dönecek şekilde başlatıldı:
0.1 s request: GET /categories
0.1 s request: GET /books/9780441172719
0.1 s request: GET /books/9780441172719/reviews
0.1 s console: [book] loading | [related] loading
0.1 s console: [reviews] loading: 0 reviews
0.4 s request: GET /books?category=science-fiction
0.4 s console: [book] resolved | [related] loading
0.4 s console: [reviews] resolved: 2 reviews
0.7 s console: [book] resolved | [related] resolved
--- user adds a review
1.6 s request: POST /books/9780441172719/reviews
1.6 s console: [reviews] local: 3 reviews
screen right after click: 3 reviews
1.9 s console: Failed to load resource: the server responded with a status of 503 (Service Unavailable)
1.9 s console: [reviews] local: 2 reviews
screen: Ada (5/5): Still the best world-building I know. | Alan (4/5): Slow start, great second half.
error : Could not save your review. Please try again.
Yorum yine anında göründü (local: 3 reviews), 503 gelince liste eski hâline döndü (local: 2 reviews) ve kullanıcı bir hata mesajı gördü. Status'ün error değil local olduğuna dikkat: resource'un kendi isteği hiç başarısız olmadı, başarısız olan bizim POST'umuz; geri alma da bir set olduğu için status local. Hata mesajı bu yüzden resource'ta değil, component'in kendi saveError signal'ında duruyor.
Optimistic update her mutation için doğru seçim değil. Yorum gibi geri alınması kolay ve başarısız olma ihtimali düşük işlemlerde kullanıcıya hız kazandırıyor. Sipariş vermek ya da ödeme gibi işlemlerde ise ekranda "tamamlandı" gösterip sonra geri almak kullanıcıyı yanıltır; orada butonu kilitleyip sunucunun cevabını beklemek doğru.
Aynı Veriyi İki Kez Çekmek: Paylaşılan Resource
Header'a bir kategori menüsü ekledik; katalog sayfasının filtre linkleri de aynı listeyi kullanıyor. İlk denemede iki component de kendi httpResource'unu oluşturuyordu:
import { httpResource } from '@angular/common/http';
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import { apiUrl } from '../api';
import type { Category } from '../models';
@Component({
selector: 'app-category-menu',
imports: [RouterLink],
template: `
<nav class="category-menu">
@for (c of categories.value(); track c.slug) {
<a routerLink="/" [queryParams]="{ category: c.slug }">{{ c.name }}</a>
}
</nav>
`,
})
export class CategoryMenu {
// her component kendi kopyasını çekiyor
protected readonly categories = httpResource<Category[]>(() => `${apiUrl}/categories`, {
defaultValue: [],
});
}
Katalog sayfasında da aynı satır vardı. Playwright kataloğu açtı, bir kitabın detayına gitti ve geri döndü; API'ye giden istekler:
GET /api/categories
GET /api/books?page=1
GET /api/categories
GET /api/books/9780441172719
GET /api/books/9780441172719/reviews
GET /api/books?category=science-fiction
GET /api/books?page=1
GET /api/categories
=> 3 of 8 requests went to /api/categories
Değişmeyen bir liste için üç istek: biri menüden, biri katalogdan, biri de kataloğa geri dönüşte yeniden oluşan component'ten. Resource'lar birbirinden habersiz; Angular'da bunları birleştiren global bir cache katmanı yok. Çözüm, server state'i de client state gibi bir sahibe vermek: listeyi root'taki bir service'te tek bir resource olarak tutmak.
import { httpResource } from '@angular/common/http';
import { Service } from '@angular/core';
import { apiUrl } from '../api';
import type { Category } from '../models';
// Kategorileri gösteren her component'in paylaştığı server state: tek istek, tek kopya
@Service()
export class Categories {
readonly #list = httpResource<Category[]>(() => `${apiUrl}/categories`, { defaultValue: [] });
readonly list = this.#list.asReadonly();
}
Menü ve katalog inject(Categories) ile aynı resource'u okuyor; dışarıya asReadonly() ile açılmış bir Resource veriliyor, yani bir component listeyi set ile bozamıyor. Aynı senaryo:
GET /api/categories
GET /api/books?page=1
GET /api/books/9780441172719
GET /api/books/9780441172719/reviews
GET /api/books?category=science-fiction
GET /api/books?page=1
=> 1 of 6 requests went to /api/categories
Tek istek. Bunun bedeli, listenin artık uygulama boyunca bir kez yüklenip hiç tazelenmemesi; kategoriler için kabul edilebilir, stok için değil. Root'ta paylaşılan bir resource, ilk yazıdaki "global kopya" sorununa ancak tazeleme kuralı da tanımlandığında dönüşmez: belli olaylarda reload() çağırmak ya da bayatlama süresiyle yeniden çekmek. Bu kuralı her listeye elle yazmak gerekiyorsa, aşağıda değineceğimiz TanStack Query gibi bir araç ciddi bir alternatif.
NgRx 22 Resource Extension'ları
Katalog listesi sayfalı ve sayfa değişince resource loading'e geçiyor. Angular'ın tanımına göre loading'de value() undefined; yani kullanıcı "Next"e bastığı anda liste kayboluyor ve yerine "Loading..." geliyor. Düz httpResource ile "classics"in 2. sayfasına geçiş:
0.1 s console: [books] loading: -
0.4 s console: [books] resolved: Emma,Frankenstein
--- click Next
1.1 s console: [books] loading: -
screen 150 ms later: 0 books listed, "Loading..." shown
1.4 s console: [books] resolved: Middlemarch,Pride and Prejudice
screen after loading: "4 books, page 2 of 2" | Middlemarch, Pride and Prejudice
NgRx 22 bu tür davranışları resource'u değiştirmeden düzenleyen @ngrx/signals/resource paketini getirdi. API'ler NgRx dokümantasyonunda experimental olarak işaretli: sonraki sürümlerde breaking change duyurusu olmadan değişebilir. extendResource bir resource alıp aynı tipte döndürüyor; withPreviousValueOnLoading() yükleme sırasında son değeri tutuyor:
// Server state: API'deki verinin kopyası, kategori ya da sayfa değişince yeniden çekilir;
// sonraki sayfa yüklenirken önceki sayfa ekranda kalır (NgRx resource extension)
protected readonly books = extendResource(
httpResource<BookPage>(() => {
const params: Record<string, string | number> = { page: this.page() };
const category = this.category();
if (category) params['category'] = category;
return { url: `${apiUrl}/books`, params };
}),
withPreviousValueOnLoading(),
);
0.1 s console: [books] loading: -
0.4 s console: [books] resolved: Emma,Frankenstein
--- click Next
1.1 s console: [books] loading: Emma,Frankenstein
screen 150 ms later: 2 books listed, "Updating..." shown
1.4 s console: [books] resolved: Middlemarch,Pride and Prejudice
screen after loading: "4 books, page 2 of 2" | Middlemarch, Pride and Prejudice
Status yine loading, ama value() önceki sayfayı (Emma, Frankenstein) veriyor; ekran 150 ms sonra hâlâ iki kitap ve küçük bir "Updating..." yazısı gösteriyor. Template'te değişen tek şey, isLoading() doğruyken eklenen bu yazı; bu extension'dan türetilen computed'larda bir ayrıntı var, "Sık Düşülen Tuzaklar"da. Diğer extension'lar: withValueOnLoading(x) yükleme sırasında sabit bir değer, withPreviousValueOnError() hata durumunda son başarılı değer, withValueOnError(x) hata durumunda sabit bir değer döndürüyor; son ikisi value()'nun hata fırlatmasını da engelliyor. provideResourceExtensions(...) ile bunları bir injector seviyesinde (uygulama, route, component) varsayılan hâle getirmek de mümkün. Angular'ın kendisinde aynı işi yapabilen resourceFromSnapshots var, ama Angular 22'de o da experimental.
Üretimde Dikkat
SSR kullanıyorsan resource'un id seçeneği, sunucuda çekilen değeri TransferState'e yazıp tarayıcıda resource'u doğrudan resolved başlatıyor; hydration (sunucuda üretilen HTML'in tarayıcıda uygulamaya bağlanması) sırasında aynı istek ikinci kez gitmiyor. Bu değer sayfanın HTML'ine gömülüyor. Angular dokümantasyonu bu yüzden, render'ı tetikleyen kullanıcıya özel veri yükleyen resource'larda id kullanmamayı açıkça öneriyor, özellikle HTML cache'lenebiliyorsa. Kitap listesi için id uygun; "siparişlerim" ya da sepet için değil. Bookstore'un bu seride SSR'ı yok; bu paragraf dokümantasyona dayanıyor, çalıştırılmadı.
Resource'lar arası paylaşım, istek tekilleştirme (aynı anda iki component aynı URL'i isterse tek istek), bayatlama süresi, pencereye odaklanınca yeniden çekme ve mutation sonrası anahtar bazlı invalidation gibi ihtiyaçlar büyüyorsa, bunları Angular'ın resource'larının üstüne elle yazmak yerine TanStack Query'nin Angular adaptörü düşünülebilir. Paket Kasım 2026'da hâlâ @tanstack/angular-query-experimental adıyla yayımlanıyor (npm'deki son sürüm 5.107.1); adı API'sinin henüz stable kabul edilmediğini söylüyor. Bu seride kullanmıyoruz ve çalıştırmadık; karar yazısında araç seçimi ekseninde tekrar ele alınacak.
Her resource bir HTTP isteği demek. Bir sayfada kaç resource olduğunu ve hangilerinin birbirini beklediğini (chain) bilmek, sayfanın ne kadar sürede dolduğunu da belirliyor. Kitap detay sayfasında kitap ve yorumlar paralel, ilgili kitaplar ise kitaba bağlı; yani sayfanın son parçası art arda giden iki isteğin sonunda geliyor. Network sekmesinin waterfall görünümü bu zinciri doğrudan gösterir.
Sık Düşülen Tuzaklar
value()'yu error durumunda okumak
Template'i kısaltmak için başlığı {{ book.value()?.title ?? 'Loading...' }} diye yazdığını düşün. Sahte API ilk kitap isteğine 503 dönecek şekilde başlatıldı:
0.1 s request: GET /categories
0.1 s request: GET /books/9780441172719
0.1 s request: GET /books/9780441172719/reviews
0.4 s console: Failed to load resource: the server responded with a status of 503 (Service Unavailable)
0.4 s console: ERROR ResourceValueError: Resource is currently in an error state (see Error.cause for details): Http failure response for http://localhost:5280/api/books/9780441172719: 503 Service Unavailable
screen h1: Loading... | error: - | reviews listed: 0
Optional chaining (?.) işe yaramadı, çünkü sorun undefined değil: value()'nun kendisi ResourceValueError fırlattı. Başlık "Loading..." olarak kaldı ve hatayla birlikte template'in geri kalanı da render edilemedi: sunucunun başarıyla döndürdüğü iki yorum bile ekranda yok. hasValue() ve error() ile korunan asıl sayfa aynı senaryoda:
0.1 s request: GET /categories
0.1 s request: GET /books/9780441172719
0.1 s request: GET /books/9780441172719/reviews
0.4 s console: Failed to load resource: the server responded with a status of 503 (Service Unavailable)
screen h1: - | error: Could not load this book. | reviews listed: 2
Extension'lı bir resource'tan computed türetmek
Katalogdaki "4 books, page 2 of 2" özeti bir computed. İlk hâlinde yalnız hasValue() ve value()'yu okuyordu. Resource withPreviousValueOnLoading() ile sarıldıktan sonra aynı "Next" senaryosu:
0.1 s console: [books] loading: -
0.4 s console: [books] resolved: Emma,Frankenstein
--- click Next
1.1 s console: [books] loading: Emma,Frankenstein
screen 150 ms later: 2 books listed, "Updating..." shown
1.4 s console: [books] resolved: Middlemarch,Pride and Prejudice
screen after loading: "4 books, page 1 of 2" | Middlemarch, Pride and Prejudice
Liste 2. sayfayı gösteriyor, özet hâlâ "page 1 of 2" diyor ve öyle kalıyor. NgRx 22.0.1'in kaynak koduna bakınca sebep görünüyor: extension, value'yu bir Proxy ile değiştiriyor ve resource yüklenirken asıl signal'ı hiç çağırmadan saklı değeri döndürüyor; isLoading'i de untracked okuyor. Yükleme sırasında hesaplanan bir computed'ın böylece takip ettiği hiçbir signal kalmıyor ve bir daha hesaplanmıyor. Template etkilenmiyor, çünkü template isLoading()'i ayrıca okuyor. Düzeltme, computed'ın içinde önce takip edilen bir signal okumak:
// Derived state: computed from other state, never stored
protected readonly summary = computed(() => {
// status() bilerek önce okunuyor: yükleme sırasında extension'lı value() takip edilmiyor
if (this.books.status() === 'error' || !this.books.hasValue()) return '';
const { total, page, pageSize } = this.books.value();
return `${total} books, page ${page} of ${Math.ceil(total / pageSize)}`;
});
protected readonly hasNextPage = computed(() => {
if (this.books.status() === 'error' || !this.books.hasValue()) return false;
const { total, page, pageSize } = this.books.value();
return page * pageSize < total;
});
Bu davranış experimental bir API'nin bugünkü hâli; sonraki sürümlerde düzelebilir. Kural olarak, extension'lı bir resource'tan türetilen her computed'ı yükleme geçişiyle birlikte en az bir kez çalıştırarak doğrula.
Server state'i bir store'a kopyalayıp senkron tutmaya çalışmak
Resource'un değerini bir effect ile başka bir signal'a ya da store'a yazıp "uygulamanın geri kalanı oradan okusun" demek, ilk yazıdaki bayat kopya sorununu geri getiriyor; üstüne loading ve error bilgisi de yolda kayboluyor. Paylaşmak gerekiyorsa resource'un kendisini paylaş: Categories service'indeki gibi.
Mutation için httpResource kullanmak
Resource, parametreleri değişince isteği kendisi tekrar atıyor ve eski isteği iptal ediyor. Bir POST'u bu modele koymak, bir signal değiştiğinde siparişin ya da yorumun tekrar gönderilmesi ya da yolda iptal edilmesi demek. Okuma resource'la, yazma HttpClient ile.
Optimistic update'i geri almayı unutmak
catch bloğundaki set(before) olmasa, başarısız yorum listede kalır ve kullanıcı kaydedildiğini düşünür. Geri alma için "önceki hâli" optimistic update'ten hemen önce saklamak gerekiyor; sonradan sunucudan yeniden çekmek de bir seçenek, ama arada kullanıcı hatalı listeyi görür.
Küçük Bir Deneme
★☆☆ Kitap detay sayfasına bir "Refresh reviews" butonu ekle ve reviews.reload()'a bağla. Butona basınca status hangi değerlerden geçiyor? Liste ekranda kalıyor mu?
★★☆ Categories service'ine bir refresh() method'u ekle ve onu yalnız kategori menüsü 10 dakikadan uzun süre önce yüklendiyse reload() çağıracak şekilde yaz. Zamanı test edilebilir tutmak için şimdiki zamanı nereden almalısın?
★★★ Sepete ekleme butonuna optimistic bir stok düşüşü ekle: kullanıcı "Add to cart"a basınca katalogdaki stok bir azalsın, sunucu tarafında stok yetmezse (sahte API'ye bir POST /api/cart/reserve endpoint'i ekleyerek) geri alınsın. Katalog listesi extendResource ile sarılı olduğu için update ve set hâlâ çalışıyor mu?
Kataloğun sayfa numarası hâlâ bellekte bir linkedSignal: sayfayı yenileyince 1'e dönüyor, link paylaşılınca kayboluyor. Bir sonraki yazıda katalog filtrelerini ve sayfayı URL'e taşıyoruz: withComponentInputBinding() ile query param'ları input'a bağlamak, Angular 22'nin queryParams ve unmatchedInputBehavior seçenekleri, queryParamsHandling: 'merge', arama kutusunda geçmişi kirletmeyen replaceUrl ve URL'den gelen string'i doğrulamak.