Mert Özen Her Satırda Daha İleri
Yazılım Mimarisi UI Mimari Pattern'leri — #07

Angular'da Unidirectional Data Flow: Signal, input/output ve Service'te State

Mert Özen 8 Kas 2026 15 dk 2 görüntülenme
Angular'da Unidirectional Data Flow: Signal, input/output ve Service'te State

Angular'da unidirectional data flow, state'in tek sahibinin olması, verinin input() ile aşağı inmesi, değişiklik isteklerinin output() ile yukarı çıkması ve türetilmiş değerlerin computed() ile hesaplanmasıdır. Effect ile yazılan state'in geride kaldığını, model()'in iş kuralını atlattığını ve default OnPush'un yerinde değiştirilen array'i görmediğini gerçek testlerle gösteriyor, state'i signal tutan bir service'e taşıyoruz.

Sürüm notu: Bu yazıdaki örnekler Angular 22 ve TypeScript ile denenmiştir.

Önce Tahmin Et

Önceki yazıda unidirectional data flow'u framework'süz kurduk ve derived değerleri state'e yazmanın açtığı glitch'i gördük. Angular'da aynı hatanın en yaygın hâli şu: bir signal değişince başka bir signal'ı effect() ile güncellemek. Aşağıdaki component toplam tutarı iki yoldan hesaplıyor; biri effect ile yazılan bir signal, diğeri computed().

import { Component, computed, effect, signal } from '@angular/core';

@Component({
    selector: 'app-price-preview',
    template: `{{ totalFromEffect() }} / {{ totalFromComputed() }}`,
})
export class PricePreview {
    readonly quantity = signal(1);

    // Kötü örnek: derived state'i effect ile yazmak
    readonly totalFromEffect = signal(0);

    // Doğrusu: derived state'i computed ile türetmek
    readonly totalFromComputed = computed(() => this.quantity() * 14);

    constructor() {
        effect(() => this.totalFromEffect.set(this.quantity() * 14));
    }
}

Testi de şöyle:

import { TestBed } from '@angular/core/testing';
import { PricePreview } from './price-preview';

describe('PricePreview', () => {
    it('effect lags behind, computed does not', async () => {
        const fixture = TestBed.createComponent(PricePreview);
        await fixture.whenStable();
        const preview = fixture.componentInstance;

        preview.quantity.set(3);
        const read = `${preview.totalFromEffect()} / ${preview.totalFromComputed()}`;

        TestBed.tick();
        const afterTick = `${preview.totalFromEffect()} / ${preview.totalFromComputed()}`;

        console.log(`read=${read} afterTick=${afterTick}`);
        expect(read).toBe('14 / 42');
        expect(afterTick).toBe('42 / 42');
    });
});

quantity.set(3) satırından hemen sonra okunan read değeri ne olur? TestBed.tick() sonrasında okunan afterTick ne olur? İki yolun ekranda aynı sonucu verdiğini düşünüyorsan, aradaki farkın nerede ortaya çıkacağını da tahmin etmeye çalış.

Data Down, Events Up

Angular'da unidirectional data flow'un temel kuralı iki kelimeyle anlatılıyor: veri aşağı, event'ler yukarı. Parent component state'in sahibidir ve child'a değeri input() ile verir. Child bir şeyin değişmesini istediğinde değeri kendisi değiştirmez, output() ile parent'a bildirir. Parent karar verir, state'i günceller ve yeni değer tekrar aşağı iner. Flux'taki action → store → view döngüsünün component ağacındaki karşılığı bu.

import { Component, input, output } from '@angular/core';

@Component({
    selector: 'app-quantity-stepper',
    template: `
        <button type="button" (click)="quantityChange.emit(quantity() - 1)">-</button>
        <span>{{ quantity() }}</span>
        <button type="button" (click)="quantityChange.emit(quantity() + 1)">+</button>
    `,
})
export class QuantityStepper {
    // Veri aşağı: değeri parent verir
    readonly quantity = input.required<number>();
    // Event yukarı: yeni değeri parent'a önerir, kendisi değiştirmez
    readonly quantityChange = output<number>();
}

QuantityStepper adedi bilmiyor, yalnız gösteriyor. Butona basıldığında "yeni değer şu olsun" diyor ve orada bitiyor. 10'dan fazla olamaz kuralı, stok sınırı ya da kitap değişince adedin sıfırlanması bu component'in sorunu değil. input()'un döndürdüğü signal salt okunur olduğu için, child'ın this.quantity.set(5) yazması zaten derlenmiyor; framework sahipliği kodda zorluyor.

Bu modelin ilk denemede şaşırtan bir sonucu var. Testte butona arada render beklemeden art arda 12 kez bastığımızda adet 2'de kaldı. Sebep basit: child her tıklamada quantity() + 1 öneriyor ama quantity(), parent yeniden render edip yeni değeri aşağı indirene kadar eski değerinde. Gerçek kullanıcı tıklamaları arasında render olduğu için bu durum tarayıcıda yaşanmıyor; ama child'ın gördüğü değerin her zaman parent'ın verdiği değer olduğunu, kendi tahmini olmadığını iyi gösteriyor.

Kitap seçimi de aynı şekilde:

import { Component, input, output } from '@angular/core';
import type { Book } from './book';

@Component({
    selector: 'app-book-picker',
    template: `
        @for (book of books(); track book.id) {
            <button type="button" [class.selected]="book.id === selectedId()" (click)="bookSelected.emit(book)">
                {{ book.title }}
            </button>
        }
    `,
})
export class BookPicker {
    readonly books = input.required<readonly Book[]>();
    readonly selectedId = input<number | null>(null);
    readonly bookSelected = output<Book>();
}

Output adlarına dikkat et: bookSelected, quantityChange. Angular dokümantasyonu output'lara on öneki koymamayı, DOM (Document Object Model) event'leriyle çakışan adlardan (click, change) kaçınmayı ve camelCase kullanmayı öneriyor. Angular output'ları DOM'daki event'ler gibi yukarı doğru kabarmaz (bubbling yok); event yalnız doğrudan parent'a gider. Bu da akışı takip edilebilir tutan bir kısıt: bir event'in kimden kime gittiğini template'e bakarak görüyorsun.

model() Ne Zaman?

Angular 17.2 ile gelen model(), bir input ile ona eşlik eden output'u tek satırda tanımlıyor. Component kendi değerini set ya da update ile yazabiliyor ve Angular bu yazmayı otomatik oluşturulan quantityChange output'u üzerinden parent'a iletiyor:

import { Component, model } from '@angular/core';

@Component({
    selector: 'app-quantity-input',
    template: `
        <button type="button" (click)="quantity.update(q => q + 1)">+</button>
        <span>{{ quantity() }}</span>
    `,
})
export class QuantityInput {
    // model(): input + otomatik quantityChange output'u
    readonly quantity = model.required<number>();
}
import { Component, signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { QuantityInput } from './quantity-input';

@Component({
    imports: [QuantityInput],
    template: `<app-quantity-input [(quantity)]="quantity" />`,
})
class Host {
    readonly quantity = signal(10);
}
$ npx ng test --watch=false --reporters=verbose --include src/app/p07/quantity-input.spec.ts
stdout | src/app/p07/quantity-input.spec.ts > QuantityInput > writes straight into the parent signal
parent quantity: 11
 ✓ |bookstore| src/app/p07/quantity-input.spec.ts > QuantityInput > writes straight into the parent signal

Önce şunu netleştirelim: Angular'ın [( )] syntax'ı AngularJS'in two-way binding'i değil. Derleyici onu bir property binding ile bir event binding'e açıyor; runtime'da veri yine aşağı, event yine yukarı gidiyor. Digest loop yok, birbirini tetikleyen watcher zinciri yok. Yani model() akışı bozmuyor, akışı kısaltıyor.

Ama testteki sonuca bak: parent 10 ile başlıyor, artı butonuna bir kez basıyoruz ve parent'taki adet 11 oluyor. Önceki yazıdaki reducer'da ve az sonra yazacağımız state service'te adet 10'da sınırlanıyor; burada child değeri doğrudan parent'ın signal'ına yazdığı için kural hiç çalışmadı. model()'in doğal yeri, kendi değerinin tek sahibi olan form control benzeri leaf component'ler: tarih seçici, renk seçici, kendi içinde açılıp kapanan bir panelin open durumu. Angular dokümantasyonu da örnek olarak custom form control'leri veriyor. Değerin arkasında iş kuralı varsa, child'a bir writable signal uzatmak yerine output ile "yeni değer önerisi" göndermek kuralları tek yerde tutuyor.

computed() ile Derived State

Açılıştaki testin gerçek çıktısı:

$ npx ng test --watch=false --reporters=verbose --include src/app/p07/price-preview.spec.ts
stdout | src/app/p07/price-preview.spec.ts > PricePreview > effect lags behind, computed does not
read=14 / 42 afterTick=42 / 42
 ✓ |bookstore| src/app/p07/price-preview.spec.ts > PricePreview > effect lags behind, computed does not

quantity.set(3)'ten hemen sonra computed 42 verirken effect ile yazılan signal hâlâ 14 gösteriyor. Effect'ler senkron çalışmaz; Angular onları sıraya koyar ve change detection sırasında çalıştırır. Testte TestBed.tick() bu sırayı boşaltınca değer 42'ye yetişiyor. Bu arada okuyan herkes (bir başka computed, bir service method'u, bir test) eski değeri görüyor. Önceki yazıdaki total=42.00 discount=0.00 satırı ile aynı problem, yalnız bu sefer Angular'ın zamanlamasıyla.

computed() bu problemi tasarım gereği yaşamıyor. Angular dokümantasyonuna göre computed signal'lar tembel (lazy) hesaplanır ve memoize edilir: okunduğu anda bağımlılıklarından biri değişmişse yeniden hesaplanır, değişmemişse önbellekteki değeri döner. Okuma ile hesaplama arasında bir zaman aralığı olmadığı için ara state de yok. Kural basit: bir değer başka signal'lardan türetilebiliyorsa onu state'e yazma, computed() ile türet.

Bazen türetilmiş değerin kullanıcı tarafından da değiştirilebilmesi gerekir. Seçilen kitap değişince adet 1'e dönsün, ama arada kullanıcı adedi artırabilsin. Bunun için effect yazmak yerine Angular 19'da gelen linkedSignal() var:

import { Component, input, linkedSignal } from '@angular/core';
import type { Book } from './book';

@Component({
    selector: 'app-book-line',
    template: `{{ book().title }} x{{ quantity() }}`,
})
export class BookLine {
    readonly book = input.required<Book>();
    // Kitap değişince 1'e döner, arada kullanıcı değiştirebilir
    readonly quantity = linkedSignal({ source: this.book, computation: () => 1 });
}
$ npx ng test --watch=false --reporters=verbose --include src/app/p07/book-line.spec.ts
stdout | src/app/p07/book-line.spec.ts > BookLine > resets the quantity when the book changes
The Hobbit x3 -> Emma x1
 ✓ |bookstore| src/app/p07/book-line.spec.ts > BookLine > resets the quantity when the book changes

effect() Yalnız Side Effect İçin

Angular dokümantasyonu effect'leri açıkça "son çare" olarak konumluyor ve state'i bir signal'dan diğerine taşımak için kullanılmamaları gerektiğini, bunun ExpressionChangedAfterItHasBeenChecked hatalarına, döngüsel güncellemelere ve gereksiz change detection turlarına yol açabileceğini söylüyor. Angular 19'dan beri effect içinde signal yazmak ayrı bir izin (allowSignalWrites) gerektirmiyor; yani derleyici seni artık durdurmuyor, karar sende.

Effect'in meşru işi, signal dünyasındaki bir değişikliği signal olmayan bir dünyaya yansıtmak: localStorage'a yazmak, log atmak, signal bilmeyen bir chart library'yi güncellemek, DOM'a elle dokunmak. Bir sonraki bölümdeki state service'te taslağı tarayıcıya kaydeden effect tam olarak bu türden. Kendine şu soruyu sor: effect'in sonunda bir signal'a mı yazıyorum, yoksa dışarıdaki bir şeye mi? İlkiyse büyük ihtimalle aradığın şey computed() ya da linkedSignal().

State'i Bir Service'e Taşımak

Sipariş formu tek component'ten çıkıp bir sayfaya yayıldığında (sepet özeti header'da, form ortada, stok uyarısı başka bir yerde) state'i bir component'te tutmak yetmez. Angular'da en sade çözüm signal tutan bir service: önceki yazıdaki store'un signal'larla yazılmış hâli.

import { Injectable, computed, effect, signal } from '@angular/core';
import type { Book } from './book';

interface OrderDraft {
    readonly book: Book | null;
    readonly quantity: number;
}

@Injectable({ providedIn: 'root' })
export class OrderDraftStore {
    // State tek yerde ve dışarıya kapalı
    private readonly state = signal<OrderDraft>({ book: null, quantity: 1 });

    // Okuma: salt okunur signal'lar
    readonly book = computed(() => this.state().book);
    readonly quantity = computed(() => this.state().quantity);
    readonly subtotal = computed(() => (this.book()?.price ?? 0) * this.quantity());
    readonly discount = computed(() => (this.quantity() >= 3 ? this.subtotal() / 10 : 0));
    readonly total = computed(() => this.subtotal() - this.discount());

    constructor() {
        // Side effect: taslağı tarayıcıya kaydetmek effect'in işi
        effect(() => localStorage.setItem('order-draft', JSON.stringify(this.state())));
    }

    // Yazma: kurallar yalnız burada
    selectBook(book: Book): void {
        this.state.update(s => ({ ...s, book, quantity: 1 }));
    }

    changeQuantity(quantity: number): void {
        const clamped = Math.min(Math.max(quantity, 1), 10);
        this.state.update(s => ({ ...s, quantity: clamped }));
    }
}

Önceki yazıdaki parçalarla eşleştirirsen tablo şöyle:

Flux / ReduxSignal tutan service
Store'daki stateprivate readonly state = signal(...)
Selectorcomputed() ile açılan salt okunur signal'lar
Action + reducerselectBook(), changeQuantity() gibi public method'lar
Subscribe ve renderTemplate'te signal okumak; Angular kendisi takip ediyor
Middleware / effecteffect() ya da method içinde async iş

Kritik nokta, writable signal'ın private olması. Dışarıya yalnız computed() ile türetilmiş, salt okunur signal'lar çıkıyor; state'i değiştirmenin tek yolu service'in method'ları. Önceki yazıdaki "state is read-only, değişiklik tek kapıdan" prensibi burada TypeScript'in access modifier'larıyla sağlanıyor. Service'te bir kural değiştiğinde (indirim eşiği, adet sınırı) dokunduğun tek dosya bu.

Container component state'i okuyor ve child'lardan gelen event'leri service'e iletiyor:

import { Component, inject } from '@angular/core';
import { CurrencyPipe } from '@angular/common';
import { BOOKS } from './book';
import { BookPicker } from './book-picker';
import { OrderDraftStore } from './order-draft-store';
import { QuantityStepper } from './quantity-stepper';

@Component({
    selector: 'app-order-form',
    imports: [BookPicker, QuantityStepper, CurrencyPipe],
    template: `
        <app-book-picker [books]="books" [selectedId]="store.book()?.id ?? null" (bookSelected)="store.selectBook($event)" />
        @if (store.book()) {
            <app-quantity-stepper [quantity]="store.quantity()" (quantityChange)="store.changeQuantity($event)" />
            <p class="total">{{ store.total() | currency: 'EUR' }}</p>
        }
    `,
})
export class OrderForm {
    protected readonly store = inject(OrderDraftStore);
    protected readonly books = BOOKS;
}
$ npx ng test --watch=false --reporters=verbose --include src/app/p07/order-form.spec.ts
stdout | src/app/p07/order-form.spec.ts > OrderForm > flows data down and events up
10 €126.00
 ✓ |bookstore| src/app/p07/order-form.spec.ts > OrderForm > flows data down and events up

Testte bir kitap seçip artı butonuna 12 kez bastık (her tıklamadan sonra render bekleyerek); adet 10'da durdu ve toplam yüzde 10 indirimle 126 avro oldu. Service'in kendi testi ise TestBed'de bir component bile yaratmadan çalışıyor:

import { TestBed } from '@angular/core/testing';
import { BOOKS } from './book';
import { OrderDraftStore } from './order-draft-store';

describe('OrderDraftStore', () => {
    it('derives totals and saves the draft', () => {
        const store = TestBed.inject(OrderDraftStore);
        store.selectBook(BOOKS[0]);
        store.changeQuantity(3);
        TestBed.tick();
        console.log(`total=${store.total()} saved=${localStorage.getItem('order-draft')}`);
        expect(store.total()).toBe(37.8);
    });
});
$ npx ng test --watch=false --reporters=verbose --include src/app/p07/order-draft-store.spec.ts
stdout | src/app/p07/order-draft-store.spec.ts > OrderDraftStore > derives totals and saves the draft
total=37.8 saved={"book":{"id":1,"title":"The Hobbit","price":14},"quantity":3}
 ✓ |bookstore| src/app/p07/order-draft-store.spec.ts > OrderDraftStore > derives totals and saves the draft

NgRx Signal Store'a Kısa Bakış

Bu pattern'in standartlaşmış hâli NgRx Signal Store. Angular 22 ile uyumlu sürümü olan @ngrx/signals 22.0.1'de aynı store şöyle yazılıyor:

import { computed } from '@angular/core';
import { patchState, signalStore, withComputed, withMethods, withState } from '@ngrx/signals';
import type { Book } from './book';

interface OrderDraft {
    book: Book | null;
    quantity: number;
}

export const OrderDraftSignalStore = signalStore(
    { providedIn: 'root' },
    withState<OrderDraft>({ book: null, quantity: 1 }),
    withComputed(({ book, quantity }) => {
        const subtotal = computed(() => (book()?.price ?? 0) * quantity());
        const discount = computed(() => (quantity() >= 3 ? subtotal() / 10 : 0));
        return { subtotal, discount, total: computed(() => subtotal() - discount()) };
    }),
    withMethods(store => ({
        selectBook(book: Book): void {
            patchState(store, { book, quantity: 1 });
        },
        changeQuantity(quantity: number): void {
            patchState(store, { quantity: Math.min(Math.max(quantity, 1), 10) });
        },
    })),
);
$ npx ng test --watch=false --reporters=verbose --include src/app/p07/order-draft-signal-store.spec.ts
stdout | src/app/p07/order-draft-signal-store.spec.ts > OrderDraftSignalStore > applies the same rules as the hand-written store
quantity=10 total=126
 ✓ |bookstore| src/app/p07/order-draft-signal-store.spec.ts > OrderDraftSignalStore > applies the same rules as the hand-written store

Parçalar tanıdık: withState state'i, withComputed selector'ları, withMethods action'ları tanımlıyor ve state yalnız patchState ile, yeni bir object üreterek güncelleniyor. Store'un state'i varsayılan olarak korumalı. Bir component kestirmeden gidip state'i dışarıdan yamamaya çalışırsa:

    protected setTen(): void {
        // Kötü örnek: store'un method'larını atlayıp state'i dışarıdan yamamak
        patchState(this.store, { quantity: 10 });
    }
$ npx tsc -p tsconfig.app.json --noEmit
src/app/p07/quick-fix.ts(14,20): error TS2345: Argument of type '{ book: DeepSignal<Book> | Signal<null>; quantity: Signal<number>; subtotal: Signal<number>; discount: Signal<number>; total: Signal<...>; selectBook: (book: Book) => void; changeQuantity: (quantity: number) => void; } & StateSource<...>' is not assignable to parameter of type 'WritableStateSource<{ book: Book | null; quantity: number; }>'.
  The types of '[STATE_SOURCE].book' are incompatible between these types.
    Type 'Signal<Book | null>' is missing the following properties from type 'WritableSignal<Book | null>': set, update, asReadonly, [ɵWRITABLE_SIGNAL]

Elle yazdığımız service'te private ile sağladığımız korumayı NgRx tip seviyesinde veriyor. Peki ne zaman gerekli? Birkaç store'un aynı kalıpla yazıldığı, entity koleksiyonlarının (withEntities) yönetildiği, RxJS tabanlı akışların (rxMethod) store'a bağlandığı, lifecycle hook'larına ve ekip içinde ortak bir yapıya ihtiyaç duyulan uygulamalarda. Tek bir formun taslağı için yukarıdaki service fazlasıyla yeterli. NgRx'in Store, Effects ve Signal Store API'lerinin (Application Programming Interface) tam anlatımı, ilerideki Frontend kategorisindeki Angular'da State Yönetimi serisinin konusu; burada yalnız pattern'in aynı olduğunu görmek yeterli.

Angular 22'de Default OnPush ve Bu Model

Angular 22 ile strateji belirtmeyen her component OnPush davranıyor; eski davranış artık ChangeDetectionStrategy.Eager adıyla açıkça istenmesi gereken bir seçenek. OnPush bir component'i yalnız belirli durumlarda kontrol ediyor: input'una yeni bir referans geldiğinde, template'inde okunan bir signal değiştiğinde, kendi içinde bir event olduğunda ya da açıkça işaretlendiğinde. Unidirectional data flow ile bu kurallar birebir örtüşüyor. Aynı child'ı iki farklı parent ile deneyelim:

import { Component, input } from '@angular/core';

@Component({
    selector: 'app-cart-count',
    template: `{{ titles().length }} items`,
})
export class CartCount {
    readonly titles = input.required<readonly string[]>();
}
import { Component, signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { CartCount } from './cart-count';

@Component({
    imports: [CartCount],
    template: `<app-cart-count [titles]="titles" /><button (click)="add()">add</button>`,
})
class MutatingCart {
    readonly titles: string[] = ['The Hobbit'];

    add(): void {
        // Aynı array'e push: referans değişmiyor
        this.titles.push('Emma');
    }
}

@Component({
    imports: [CartCount],
    template: `<app-cart-count [titles]="titles()" /><button (click)="add()">add</button>`,
})
class ImmutableCart {
    readonly titles = signal<readonly string[]>(['The Hobbit']);

    add(): void {
        // Yeni array: referans değişiyor
        this.titles.update(list => [...list, 'Emma']);
    }
}
$ npx ng test --watch=false --reporters=verbose --include src/app/p07/cart-count.spec.ts
 ✓ |bookstore| src/app/p07/cart-count.spec.ts > CartCount with default OnPush > mutating parent shows 1 items
 ✓ |bookstore| src/app/p07/cart-count.spec.ts > CartCount with default OnPush > immutable parent shows 2 items

Array'i yerinde değiştiren parent'ta child "1 items" göstermeye devam ediyor: parent kendi event'iyle kontrol ediliyor, ama child'a giden referans aynı olduğu için Angular child'ı atlıyor. Yeni array üreten parent'ta signal değişiyor, input'a yeni referans gidiyor, child güncelleniyor. Önceki yazıdaki "değişiklik yoksa aynı referans döner" kuralının Angular'daki karşılığı bu: immutable güncelleme, OnPush'un değişikliği görebilmesinin yolu.

Signal'lar, service'te tutulan state ve immutable güncellemeyle kurulmuş bir uygulamada default OnPush'a geçiş çoğu zaman fark edilmiyor bile. Fark edildiği yer, state'in yerinde değiştirildiği eski kod. O yüzden Angular 22'ye yükseltirken ng update'in eski component'lere Eager eklemesi bir geçiş kolaylığı; hedef, o satırları zamanla kaldırabilecek bir veri akışına sahip olmak.

Sık Düşülen Tuzaklar

Effect içinde state yazmak. Açılıştaki test bunun en küçük örneği. Effect bir signal'ı okuyup başka bir signal'a yazıyorsa, o değer ya computed() ya da linkedSignal() olmalı. Effect'in sonu signal olmayan bir yere çıkmalı.

model()'i her yerde kullanmak. Her seviyede [(...)] ile bağlanmış bir ağaçta herkes herkesin değerini yazabiliyor ve kurallar child'lara dağılıyor; ilk yazıdaki Smart UI (user interface; bütün mantığın arayüz koduna gömüldüğü yaklaşım) sorununun signal'lı hâli. model()'i değerinin tek sahibi olan leaf component'lere bırak.

Writable signal'ı service'ten dışarı açmak. readonly quantity = signal(1) şeklinde public bir writable signal, service'in method'larını atlayan bir arka kapı. Dışarıya computed() ya da asReadonly() ile salt okunur signal ver.

Input'u state gibi kullanmak. Child'ın aldığı input'u kendi içinde değiştirmeye çalışmak (decorator tabanlı @Input() ile hâlâ mümkün) parent ile child'ın aynı değerin iki farklı kopyasını tutmasına yol açar. Signal tabanlı input() bunu derleme anında engelliyor; yerel bir kopya gerekiyorsa linkedSignal() kullan.

Her şeyi bir service'e taşımak. Bir dropdown'un açık olup olmadığı, bir input'un henüz kaydedilmemiş değeri component'in kendi state'idir. State'i, onu okuyan en yakın ortak sahibin yanına koy.

Küçük Bir Deneme

★☆☆ PricePreview'daki effect'i kaldır ve totalFromEffect'i silip testi yalnız computed ile yeniden yaz. Sonra component'e bir kargo ücreti signal'ı ekle (toplam 50'nin altındaysa 5) ve onu da computed ile türet.

★★☆ QuantityInput'u OrderForm'da kullanmak istiyorsun ama 10 sınırı bozulmasın. İki yol dene: model()'e max input'u eklemek ve model() yerine input + output kullanıp kararı store'a bırakmak. Hangisinde kural tek yerde kalıyor?

★★★ OrderDraftStore'a submit() method'u ekle: durum submitting'e geçsin, bir HTTP (Hypertext Transfer Protocol) isteği gönderilsin, cevap gelince submitted olsun ve taslak localStorage'dan silinsin. Aynı store'u NgRx Signal Store ile yaz ve iki versiyonun testlerini karşılaştır.

Bu yazıda akışı Angular'ın kendi araçlarıyla kurduk ve state'i bir service'e taşıdık. Bir sonraki yazıda component ağacının kendisine bakıyoruz: hangi component state'i bilmeli, hangisi yalnız input alıp event yaymalı? Container ve presentational component ayrımını, component'in public API'sini bir contract olarak tasarlamayı ve content projection ile composition'ı işleyeceğiz.