Mert Özen Her Satırda Daha İleri
Frontend Angular'da State Yönetimi — #01

Angular'da State Nedir ve Nerede Yaşamalı? Server, UI, URL ve Form State'ini Ayırmak

Mert Özen 14 Kas 2026 14 dk 9 görüntülenme
Angular'da State Nedir ve Nerede Yaşamalı? Server, UI, URL ve Form State'ini Ayırmak

State'in yeri iki soruyla belirlenir: veri kime ait (API, adres çubuğu, tarayıcı) ve ne kadar yaşamalı; server state resource'ta, URL state query param'da, local state component'te, paylaşılan client state bir service'te durur, derived değerler ise hiç saklanmaz. Bookstore'un katalog sayfasında dört türün geri tuşu ve yenilemede ne olduğunu gerçek bir koşuyla gösteriyor, API yanıtını global store'a kopyalamanın bayat stok ürettiğini kanıtlıyoruz.

Sürüm notu: Bu yazıdaki örnekler Angular 22.2, 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.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) Angular istemcisi bookstore-web'de bir katalog sayfası var. Kullanıcı şunları sırayla yapıyor: /?category=classics adresini açıyor, Emma'nın kapağına tıklayıp büyük kapak dialog'unu açıyor, Emma'yı sepete ekliyor, header'daki "Cart" link'iyle sepete gidiyor, tarayıcının geri tuşuna basıyor ve son olarak sayfayı yeniliyor.

Her adımdan sonra dört şeye bak: seçili kategori, kitap listesi (ve bunun için API'ye yeni istek gidip gitmediği), header'daki sepet sayısı, kapak dialog'unun açık olup olmadığı. Geri tuşundan sonra dialog açık mı? Yenilemeden sonra sepette kaç kitap var? Toplamda API'ye kaç istek gitti? Tahminini bir kenara yaz; cevap birkaç bölüm sonra gerçek bir koşunun çıktısında.

Bu dört şeyin her biri state, ama dördü de farklı yerlerde yaşıyor ve farklı sürelerde ölüyor. Seri boyunca kuracağımız her şey bu ayrıma dayanıyor: hangi state'in kime ait olduğu ve ne kadar yaşaması gerektiği. Signal, computed, linkedSignal ve httpResource API'lerinin kendisini Angular serisi anlattı ("Angular 16 Yenilikleri: Signals, Required Input ve takeUntilDestroyed", "Angular 19: Varsayılan Standalone, linkedSignal ve resource() ile Veri Yükleme", "Angular 20: Stable Signal API'leri, httpResource ve Yeni Dosya Adlandırma"); burada bu araçları bir uygulamanın state mimarisinde nereye koyacağımıza bakıyoruz.

State Nedir ve Neden Türlere Ayrılır?

Bir frontend uygulamasında state, ekranın gösterdiği ve zamanla değişen her veridir: kitap listesi, sepetteki ürünler, seçili kategori, açık bir dialog, yarım doldurulmuş bir adres formu. Bunları tek bir torbaya koymak cazip gelir; "her şeyi bir store'a koyalım, herkes oradan okusun" fikri buradan çıkar. Sorun şu ki bu veriler iki önemli soruda birbirinden çok farklı cevap veriyor.

Birinci soru sahiplik: verinin gerçek hâli nerede duruyor? Kitabın stoğu Bookstore API'sinin veritabanında duruyor; tarayıcıdaki değer yalnızca bir kopya ve o kopyayı başka bir müşteri sipariş verdiği anda bayatlatabiliyor. Sepet ise tamamen bu tarayıcıya ait; sunucu ondan, sipariş verilene kadar haberdar değil. Seçili kategori adres çubuğunda duruyor ve kullanıcı onu elle değiştirebiliyor.

İkinci soru ömür: bu veri ne zamana kadar yaşamalı? Kapak dialog'u, kullanıcı sayfadan çıkınca kapanmalı. Sepet sayfalar arasında gezerken kaybolmamalı. Kategori filtresi yenilemeden sonra da, link başka birine gönderildiğinde de aynı kalmalı. Bir state'i yanlış yere koymak, onu yanlış ömre mahkûm etmek demek: dialog geri dönüldüğünde hâlâ açık kalır ya da filtre yenilemede kaybolur.

Bu iki soru, yerleşik sınıflandırmayı da veriyor:

TürSahibiÖmrüBookstore'dan örnekAngular'daki yeri
Server stateAPI ve veritabanıSunucuda kalıcı; istemcideki kopya bayatlarKitap listesi, stok, yorumlarhttpResource, resource
URL stateAdres çubuğuYenileme, paylaşılan link, geri tuşu?category=classics, sayfa numarasıQuery param + input()
Client (UI) stateTarayıcıdaki uygulamaSahibi olan component ya da injector yaşadıkçaSepet, açık dialog, seçili sekmesignal, service
Form stateFormForm ekrandaykenCheckout adresi, yorum taslağıSignal Forms, Reactive Forms
Persistent stateTarayıcı storage'ıYenilemeyi ve sekmenin kapanmasını atlatırSepetin localStorage kopyası, temaeffect + localStorage

Server state'in kendine özgü dertleri var: yüklenirken bir "loading" hâli, başarısız olunca bir hata hâli, zamanla bayatlama ve sizin dışınızda değişme. Bunların hepsini elle bir store'da modellemek mümkün ama Angular 22'de stable olan resource ailesi tam da bu iş için var; serinin üçüncü yazısı bunu anlatıyor. URL state ise string'ten ibaret ve kullanıcının elle düzenleyebildiği bir girdi: ?page=abc gelebilir. Dördüncü yazı URL'i state olarak kullanmayı ve doğrulamayı ele alıyor.

Client state ikiye ayrılıyor: yalnız bir component'in umursadığı local state (dialog açık mı, hangi sekme seçili) ve birden fazla yerin okuduğu paylaşılan state (sepet: header'daki sayı, sepet sayfası, checkout). Form state de teknik olarak client state, ama kendi kuralları var (dirty, touched, valid, async doğrulama) ve Angular'da kendi API'si var; "Angular 22: Varsayılan OnPush, Stable Signal Forms ve Angular 12 ile Proje Farkı" yazısındaki Signal Forms bu işi yapıyor ve bu seride ayrıca anlatılmıyor. Persistent state ayrı bir tür değil, client state'e eklenen bir ömür kararı: sepet hâlâ client state, sadece bir kopyası da localStorage'da tutuluyor.

Derived State, State Değildir

Bir de state gibi görünüp state olmayan değerler var. Katalog sayfasındaki "4 books, page 1 of 2" yazısı, API'den gelen total, page ve pageSize değerlerinden hesaplanıyor. Sepetteki ürün sayısı, sepet satırlarının quantity toplamı. Sepet tutarı, satır fiyatlarının toplamı. Bunlar derived (türetilmiş) değerler: kaynakları değişince kendiliğinden değişmeleri gerekiyor.

Derived bir değeri ayrı bir field'da saklamak, onu kaynağıyla senkron tutma işini üstlenmek demek. Sepete ekleyen method sayıyı artırır, ama bir gün eklenen "adet değiştir" method'u sayıyı güncellemeyi unutur ve header "Cart (3)" derken sepet sayfası iki ürün gösterir. computed bu tür hataları ortadan kaldırıyor: değeri saklamıyorsun, nasıl hesaplanacağını tarif ediyorsun. Angular okuyan biri olduğunda hesaplıyor, kaynak değişmediği sürece sonucu tekrar kullanıyor. Kural basit: bir değer başka state'ten hesaplanabiliyorsa onu saklama, computed ile türet.

Bookstore'un State Envanteri

Katalog sayfası dört türün dördünü de aynı component'te barındırıyor. Kodun tamamı yazının snapshot'ında; çalıştırmak için kök klasörde sahte API'yi, bookstore-web'de Angular uygulamasını başlatmak yeterli:

npm run api
cd bookstore-web
npm ci
npm start

Sayfa şöyle:

import { httpResource } from '@angular/common/http';
import { Component, computed, inject, input, signal } from '@angular/core';
import { RouterLink } from '@angular/router';
import { apiUrl } from '../api';
import { CartStore } from '../cart/cart-store';
import type { Book, BookPage } from '../models';

@Component({
  selector: 'app-catalog-page',
  imports: [RouterLink],
  template: `
    <h1>Catalog</h1>
    <nav class="categories">
      <a routerLink="/">All</a>
      @for (c of categories; track c.slug) {
        <a routerLink="/" [queryParams]="{ category: c.slug }">{{ c.name }}</a>
      }
    </nav>
    @if (books.hasValue()) {
      <p class="summary">{{ summary() }}</p>
      <ul class="books">
        @for (book of books.value().items; track book.isbn) {
          <li>
            <button class="cover" (click)="coverOf.set(book)">{{ book.title }}</button>
            {{ book.author }} - {{ book.price }} TRY - {{ book.stock }} in stock
            <button class="add" (click)="cart.add(book)">Add to cart</button>
          </li>
        }
      </ul>
    } @else if (books.error()) {
      <p>Could not load the books.</p>
    } @else {
      <p>Loading...</p>
    }
    @if (coverOf(); as book) {
      <div class="cover-dialog" role="dialog">
        <p>{{ book.title }} (large cover)</p>
        <button (click)="coverOf.set(null)">Close</button>
      </div>
    }
  `,
})
export class CatalogPage {
  // URL state: ?category=classics, withComponentInputBinding() input'a bağlar
  readonly category = input<string>();

  // Server state: API'deki verinin kopyası, kategori değişince yeniden çekilir
  protected readonly books = httpResource<BookPage>(() => {
    const category = this.category();
    return { url: `${apiUrl}/books`, params: category ? { category } : undefined };
  });

  // Global client state: header ve sepet sayfasıyla paylaşılır
  protected readonly cart = inject(CartStore);

  // Local UI state: kapak dialog'unun açık olması yalnız bu sayfayı ilgilendirir
  protected readonly coverOf = signal<Book | null>(null);

  // Derived state: başka state'ten hesaplanır, hiçbir yerde saklanmaz
  protected readonly summary = computed(() => {
    if (!this.books.hasValue()) return '';
    const { total, page, pageSize } = this.books.value();
    return `${total} books, page ${page} of ${Math.ceil(total / pageSize)}`;
  });

  protected readonly categories = [
    { slug: 'science-fiction', name: 'Science Fiction' },
    { slug: 'classics', name: 'Classics' },
    { slug: 'fantasy', name: 'Fantasy' },
  ];
}

Kategori bir input(): provideRouter(routes, withComponentInputBinding()) ile router, ?category=classics query param'ını aynı isimli input'a yazıyor. Kitap listesi bir httpResource: kategori signal'ı değişince isteği kendisi yeniden atıyor, loading ve hata hâllerini de kendisi taşıyor. Sepet root'ta tek instance olan bir service'ten geliyor. Dialog bu component'in kendi signal'ı. Özet satırı computed; hiçbir yerde saklanmıyor. Sepet service'i şimdilik en basit hâliyle:

import { computed, Service, signal } from '@angular/core';
import type { Book, CartItem } from '../models';

// Global client state: bütün uygulama için tek instance (root injector)
@Service()
export class CartStore {
  readonly #items = signal<CartItem[]>([]);
  readonly items = this.#items.asReadonly();
  readonly count = computed(() => this.#items().reduce((sum, item) => sum + item.quantity, 0));

  add(book: Book): void {
    this.#items.update((items) =>
      items.some((item) => item.isbn === book.isbn)
        ? items.map((item) => (item.isbn === book.isbn ? { ...item, quantity: item.quantity + 1 } : item))
        : [...items, { isbn: book.isbn, title: book.title, price: book.price, quantity: 1 }],
    );
  }
}

Writable signal private (#items), dışarıya yalnız asReadonly() ile okunur bir signal açılıyor; değişiklik yalnız add üzerinden. Bu yapıyı ve Angular 22'nin @Service() decorator'ını bir sonraki yazı ayrıntılı anlatıyor. Header sepet sayısını aynı instance'tan okuyor:

import { Component, inject } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router';
import { CartStore } from './cart/cart-store';

@Component({
  selector: 'app-root',
  imports: [RouterOutlet, RouterLink],
  template: `
    <header>
      <a routerLink="/">Bookstore</a>
      <a class="cart-link" routerLink="/cart">Cart ({{ cart.count() }})</a>
    </header>
    <main><router-outlet /></main>
  `,
})
export class App {
  protected readonly cart = inject(CartStore);
}

Şimdi "Önce Tahmin Et"teki senaryo. Playwright her adımdan sonra sayfayı okuyup bir satır yazdı; son sütun o ana kadar API'ye giden GET sayısı:

step                      url                  catalog               header    cover dialog  API requests
open /?category=classics  /?category=classics  4 books, page 1 of 2  Cart (0)  closed        1
open Emma cover           /?category=classics  4 books, page 1 of 2  Cart (0)  open          1
add Emma to cart          /?category=classics  4 books, page 1 of 2  Cart (1)  open          1
go to cart                /cart                -                     Cart (1)  closed        1
browser back              /?category=classics  4 books, page 1 of 2  Cart (1)  closed        2
reload (F5)               /?category=classics  4 books, page 1 of 2  Cart (0)  closed        3

Her sütun bir state türünün ömrünü gösteriyor:

  • Kapak dialog'u (local UI state) sepete gidilince kayboldu ve geri dönünce de kapalıydı: coverOf signal'ı CatalogPage instance'ına ait, router component'i destroy edince o da gitti. Geri dönüşte yeni bir instance, yeni bir null.
  • Sepet (global client state) sayfa değişikliğini atlattı ama yenilemeyi atlatamadı. Root injector uygulama yaşadıkça yaşıyor; yenileme uygulamayı baştan başlatıyor ve bellek sıfırlanıyor.
  • Kategori (URL state) her şeyi atlattı: sepet sayfasından geri tuşuyla dönünce de, yenilemeden sonra da classics. Çünkü hiçbir JavaScript belleğinde değil, adres çubuğunda duruyor.
  • Kitap listesi (server state) üç kez istendi: ilk açılışta, geri dönüşte ve yenilemede. Component her oluştuğunda resource yeni bir istekle başlıyor. Bu bir hata değil; sunucudaki veri bu arada değişmiş olabilir. Ama aynı verinin iki component tarafından ayrı ayrı çekilmesi ya da gereksiz tekrar istekler server state yazısının konusu.

Tahminin nasıldı? En sık yapılan yanlış, sepetin yenilemeden sonra da duracağını düşünmek. Kullanıcı da aynı şeyi bekliyor; bu yüzden sepet ileride persistent state'e dönüşecek. Ama bunu bilerek ve tek bir yerde yapacağız, "her state localStorage'a yazılsın" diye değil.

State'in Kapsamı: Component, Feature, Root

Client state için ikinci karar kapsam. Angular'da bir state'in ömrü, onu tutan şeyin ömrüdür: component'in field'ı component'le, bir service ise onu sağlayan injector'la yaşar. Pratikte üç seviye var:

  • Component içinde: dialog, açılır menü, bir kartın "detayı göster" durumu. Component destroy olunca biter; temizlemen gereken hiçbir şey yok.
  • Feature seviyesinde: bir route'un providers listesinde sağlanan service. Kullanıcı o route ağacına ilk girdiğinde oluşur; router'da withAutoCleanupInjectors() açıksa kullanıcı ağaçtan çıkınca biter, açık değilse uygulama boyunca yaşar (bir sonraki yazı bu farkı gerçek çıktıyla gösteriyor). Checkout adımları (adres, kargo, onay) arasında paylaşılan ama checkout'tan çıkınca unutulması gereken state buraya uyar.
  • Root'ta: bütün uygulamanın paylaştığı ve uygulama yaşadıkça yaşayan state. Sepet, oturumdaki kullanıcı, tema.

Kapsamı yukarı taşımak kolay, aşağı indirmek zor; o yüzden varsayılan yön aşağıdan yukarı olmalı. Bir state'i ilk tüketicisinin yanında başlat, ikinci bir tüketici çıktığında ikisinin en yakın ortak sahibine taşı. Gereğinden yukarıdaki state'in maliyeti hemen görünmüyor ama birikiyor. Ömrü uzuyor: root'a konan bir checkout adresi, kullanıcı checkout'tan çıkıp yarım saat sonra başka bir sipariş verirken eski hâliyle karşısına çıkıyor ve onu sıfırlayan kodu elle yazman gerekiyor. Görünürlüğü artıyor: her component inject() ile ona ulaşabiliyor, kimin neyi değiştirdiğini takip etmek zorlaşıyor. Testi zorlaşıyor: tek bir component'i test ederken root'taki state'i de kurman gerekiyor.

Aşağıdaki karar ağacı bu yazının özeti. Soruları yukarıdan aşağı sorduğunda ilk "evet" state'in yerini söylüyor:

01-state-decision-tr.png

Sıralama bilinçli. Önce "hesaplanabiliyor mu" sorusu geliyor, çünkü derived bir değer hiçbir yerde saklanmamalı. Sonra sahiplik: veri API'ye aitse hangi component'te kullanıldığı fark etmeksizin server state. Ancak bu iki soru elendikten sonra kapsam sorusu anlam kazanıyor. Feature seviyesi ağacın en altında, çünkü "bir component'ten fazla ama her yer değil" durumu en az karşılaşılan ve en çok tartışılan durum; UI Mimari Pattern'leri serisinin "Büyük Angular Uygulamasında Sınırlar: Feature Folder, Facade ve Katmanlar" yazısı bu sınırları klasör ve katman açısından ele alıyor.

Üretimde Dikkat

Root'taki client state, kullanıcı değişince otomatik olarak temizlenmez. Aynı tarayıcıda çıkış yapıp başka bir hesapla giriş yapılırsa, sayfa yenilenmediği sürece sepet ve kullanıcıya özel diğer root state önceki kullanıcının verisini taşır. Çıkış akışı bu state'leri bilerek sıfırlamalı; oturum state'inin kendisi (guard, interceptor, token yenileme) Authentication ve Authorization serisinin "Angular'da Authentication State: Functional Guard'lar, Interceptor, Tek Seferlik Token Refresh ve UI Yetkilendirmesi" yazısında.

Server state'i SSR (Server-Side Rendering; sayfanın ilk HTML'inin sunucuda üretilmesi) ile kullanıyorsan, sunucuda çekilen veri sayfanın HTML'ine gömülüp tarayıcıya taşınabiliyor. Bu kitap listesi için iyi, kullanıcının siparişleri için tehlikeli: o HTML bir cache'te (önbellek) başka bir kullanıcıya gidebilir. Ayrıntısı üçüncü yazıda, resource'un id seçeneğiyle birlikte.

State mimarisini ölçmenin en ucuz yolu, bir navigation sırasında API'ye kaç istek gittiğini saymak. Yukarıdaki tabloda geri tuşu bir istek, yenileme bir istek daha üretti; bu senaryo için makul. Ama aynı sayfada iki component aynı listeyi ayrı ayrı istiyorsa ya da her sekme değişiminde liste baştan çekiliyorsa, tarayıcının Network sekmesi bunu hemen gösterir. Persistent state'te de boyut ve sekmeler arası tutarlılık önemli: iki sekmede açık bir sepetin birbirinden habersiz değişmesi ilerideki Browser API'leri ve PWA (Progressive Web App; kurulabilen ve çevrimdışı çalışabilen web uygulaması) serisinin konusu.

Sık Düşülen Tuzaklar

API yanıtını global store'a kopyalamak

En yaygın hata, server state'i client state gibi ele almak: listeyi bir kez çekip root'taki bir store'a koymak ve sonra herkesin oradan okumasını sağlamak. Kâğıt üzerinde istek sayısını azaltıyor. Bu, katalog sayfasının bu şekilde yazılmış bir versiyonu:

import { HttpClient } from '@angular/common/http';
import { inject, Service, signal } from '@angular/core';
import { apiUrl } from '../api';
import type { Book, BookPage } from '../models';

// Anti-pattern: server verisinin kimsenin tazelemediği global kopyası
@Service()
export class BookCache {
  readonly #http = inject(HttpClient);
  readonly #books = signal<Book[]>([]);
  readonly books = this.#books.asReadonly();

  loadOnce(): void {
    if (this.#books().length > 0) return; // "zaten store'da var"
    this.#http.get<BookPage>(`${apiUrl}/books`).subscribe((page) => this.#books.set(page.items));
  }
}

Katalog sayfası constructor'da loadOnce() çağırıp cache.books()'u gösteriyor. Playwright sayfayı açtı, sonra başka bir müşterinin bir Dune siparişini doğrudan API'ye gönderdi, kullanıcı sepete gidip kataloğa döndü:

catalog, first visit : Dune - 450 TRY - 12 in stock Add to cart
another customer     : POST /api/orders -> 201
catalog, second visit: Dune - 450 TRY - 12 in stock Add to cart
API says             : Dune - 11 in stock

API 11 diyor, sayfa 12 gösteriyor ve göstermeye devam edecek: store'daki kopyayı kimse tazelemiyor. Bunu düzeltmek için store'a "ne zaman bayatladı", "ne zaman yeniden çekilmeli", "yüklenirken ne gösterilmeli" mantığını eklemek gerekiyor; yani server state aracını elle yeniden yazmak. Aynı senaryo, httpResource'lu asıl sayfayla:

catalog, first visit : Dune Frank Herbert - 450 TRY - 12 in stock Add to cart
another customer     : POST /api/orders -> 201
catalog, second visit: Dune Frank Herbert - 450 TRY - 11 in stock Add to cart
API says             : Dune - 11 in stock

Kataloğa dönüş yeni bir istek üretti ve stok 11. Server state'in kopyası, onu gösteren component kadar yaşamalı; daha uzun yaşaması gerekiyorsa (aynı veriyi birçok ekran kullanıyorsa) bunu bayatlama kuralıyla birlikte yapan bir araç gerekir. Üçüncü yazı bu ikisini ayırıyor.

Derived değeri ayrıca saklamak

Sepet sayısını, toplam tutarı, "sonraki sayfa var mı" bilgisini ya da filtrelenmiş listeyi ayrı bir signal'da tutmak, her değişiklik noktasında onu da güncellemek zorunda kalmak demek. Bir noktada unutulur ve iki değer birbirini yalanlar. Bir değeri yazan tek bir method varsa ve o değer başka değerlerden çıkarılabiliyorsa, o bir computed.

Her state'i root'a koymak

"İleride başka biri de kullanabilir" diye local state'i root'a taşımak, onu ömrü boyunca sıfırlama işini de üstlenmek demek. Dialog'u root'a taşırsan geri tuşundan sonra açık kalır; bir sonraki ziyarette eski seçimle açılır. Paylaşma ihtiyacı gerçekten doğduğunda taşı, ve en yakın ortak sahibe taşı.

URL'e girmemesi gereken state'i URL'e koymak

URL güçlü bir state yeri ama her şey oraya ait değil. Açık bir dialog, bir hover durumu ya da yarım yazılmış bir yorum geçmişe kayıt yazar, geri tuşunu kullanıcı için anlamsız hâle getirir. Token, e-posta adresi gibi hassas veriler ise log'lara, tarayıcı geçmişine ve Referer header'ına sızar. URL'e paylaşılabilir ve yeniden üretilebilir olanı koy: filtre, sıralama, sayfa, seçili sekme.

Küçük Bir Deneme

★☆☆ Katalog sayfasına ?category=fantasy adresinden gir ve özet satırını kontrol et; tekil bir sonuçta metin doğru mu, değilse düzelt. Sonra adres çubuğunda kategoriyi elle poetry yap: API ne döndürüyor, sayfa ne gösteriyor? Bu, dördüncü yazıdaki "URL'den gelen girdiyi doğrulama" konusunun ilk işareti.

★★☆ coverOf signal'ını CartStore'a taşı (root) ve Playwright senaryosunu tekrar çalıştır. Geri tuşundan sonraki satırda dialog sütunu ne gösteriyor? Bu davranışı düzeltmek için hangi kodu yazman gerekirdi?

★★★ Checkout akışı için bir state envanteri çıkar: teslimat adresi formu, kargo seçenekleri (fiyatları API'den), seçili kargo yöntemi, kupon kodu, sipariş özeti ve "sipariş veriliyor" durumu. Her biri için türünü, sahibini, ömrünü ve karar ağacındaki yerini yaz. Hangileri checkout'tan çıkınca unutulmalı, hangileri yenilemeyi atlatmalı?

Bir sonraki yazıda sepeti gerçek bir store'a çeviriyoruz: private writable signal ve asReadonly(), computed ile toplam tutar, immutable güncelleme, Angular 22'nin @Service() decorator'ı, provider scope ile state ömrü, sayfa değişince sıfırlanan state için linkedSignal ve sepeti yenilemeden kurtaran tek bir effect.