Flux ve Unidirectional Data Flow: Two-Way Binding Neden Ölçeklenmedi?
Two-way binding ve birbirini güncelleyen observable model'ler, sonucu listener sırasına bağlı cascading update'ler ve hiç geçerli olmamış ara state'ler (glitch) üretir; Flux bunu tek yönlü akışla (action → dispatcher → store → view) çözdü. Aynı sipariş formunda bu glitch'i gerçek çıktıyla gösteriyor, Redux'un üç prensibini ve Elm Architecture'ı işliyor, 40 satırlık bir TypeScript store yazıp immutability'nin ve boilerplate'in bedelini tartışıyoruz.
Sürüm notu: Bu yazıdaki örnekler Angular 22 ve TypeScript ile denenmiştir.
Önce Tahmin Et
Sipariş formuna geri dönüyoruz. Kullanıcı bir kitap seçiyor, adedi giriyor, ekranın altında toplam tutar görünüyor. Bir de kural var: 3 ve üzeri adette yüzde 10 indirim. Önceki yazılarda gördüğümüz Observer Synchronization fikriyle, yani her model'in değiştiğinde kendisini dinleyenlere haber vermesiyle bu formu kurmak çok doğal görünüyor. Aşağıda dört küçük observable model var ve her biri bir sonrakini güncelliyor. View, yalnız total'ı dinliyor.
// Her model değiştiği anda listener'larına haber verir
class Observable<T> {
private listeners: ((value: T) => void)[] = [];
constructor(private current: T) {}
get value(): T {
return this.current;
}
set(next: T): void {
if (next === this.current) return;
this.current = next;
this.listeners.forEach(listener => listener(next));
}
subscribe(listener: (value: T) => void): void {
this.listeners.push(listener);
}
}
const price = 14;
const quantity = new Observable(2);
const subtotal = new Observable(quantity.value * price);
const discount = new Observable(0);
const total = new Observable(subtotal.value);
// Bağlantılar: her model bir sonrakini günceller
quantity.subscribe(q => subtotal.set(q * price));
subtotal.subscribe(s => total.set(s - discount.value));
subtotal.subscribe(s => discount.set(quantity.value >= 3 ? s / 10 : 0));
discount.subscribe(d => total.set(subtotal.value - d));
// View, total her değiştiğinde yeniden çizilir
total.subscribe(t => console.log(`render: total=${t.toFixed(2)} discount=${discount.value.toFixed(2)}`));
quantity.set(3);
Seçili kitap 14 avroluk The Hobbit; adet 2'den 3'e çıkınca ara toplam 42 oluyor ve indirim kuralı devreye giriyor. Sence konsola kaç satır yazılır ve her satırda hangi değerler görünür? İkinci soru: subtotal.subscribe ile başlayan iki satırın yerini değiştirirsen çıktı değişir mi? Bir dakika düşün, sonra devam et.
Two-Way Binding ve Cascading Update
Gerçek çıktı:
$ node out/cascade.js
render: total=42.00 discount=0.00
render: total=37.80 discount=4.20
View iki kez çiziliyor ve ilk çizim hiçbir zaman geçerli olmamış bir state'i gösteriyor: 42 tutar, sıfır indirim. Kural "3 adetten itibaren indirim var" diyor ama ekran bir anlığına bunun tersini söylüyor. Sebep, subtotal değiştiğinde iki listener'ın sırayla çalışması. İlk listener total'ı eski indirimle hesaplıyor ve view'ı tetikliyor; indirim ancak ikinci listener'da güncelleniyor. Bu tür ara ve tutarsız state'e reaktif programlama literatüründe "glitch" deniyor.
İkinci sorunun cevabı daha rahatsız edici: iki subscribe satırının yerini değiştirdiğimizde çıktı tek satıra iniyor, render: total=37.80 discount=4.20. Yani ekranda ne göreceğin, kodun hangi sırayla bağlandığına göre değişiyor. Dört model ile bunu kafanda takip edebilirsin. Kırk model, onlarca ekran ve her biri birbirini güncelleyen binding'lerle takip edemezsin.
Two-way binding bu problemi büyütür, çünkü ok iki yönlüdür: view model'i, model view'ı günceller ve bir model değişikliği başka bir binding üzerinden başka bir model'i tetikleyebilir. AngularJS bunu digest loop ile çözmeye çalışıyordu: değişiklik durana kadar watcher'ları tekrar tekrar çalıştırıyor, on turda durmazsa 10 $digest() iterations reached. Aborting! hatasıyla vazgeçiyordu. Bu hata bir bug değil, döngünün sonlanacağını garanti edemeyen bir mimarinin emniyet supabıydı.
Facebook ekibi aynı problemi chat ekranındaki okunmamış mesaj sayacında yaşadı. Flux dokümantasyonunun verdiği örnek tam olarak bu: bir thread okundu olarak işaretlendiğinde hem thread model'i hem ayrı bir "okunmamış sayısı" model'i güncellenmeli. MVC (Model-View-Controller) ile kurulmuş büyük bir uygulamada bu tür bağımlılıkların dokümantasyondaki ifadeyle karmaşık bir veri akışı ağı ve tahmin edilemeyen sonuçlar ürettiği anlatılıyor. Jing Chen'in 2014'teki F8 (Facebook'un yıllık geliştirici konferansı) konuşmasında Flux'ı tanıtırken çıkış noktası da kullanıcıya yanlış okunmamış bildirim gösteren bu sayaçtı.
Flux: Action, Dispatcher, Store, View
Flux'ın önerisi tek bir kural: veri tek yönde akar. Akış action → dispatcher → store → view şeklinde ve view'da olan bir etkileşim yeni bir action başlatarak döngüyü tekrar başlatır. Dokümantasyon bunu açıkça "two-way binding yok" diye özetliyor.

- Action:
typeve veri taşıyan sade bir object. "Ne oldu" bilgisini taşır; view'dan ya da sunucudan gelebilir. - Dispatcher: her action'ı kayıtlı tüm store'lara dağıtan merkez. Dokümantasyonun tarifiyle store'lara giden callback'lerin bir kaydı; kendi mantığı yok.
- Store: bir alanın state'ini ve o state'i değiştiren mantığı tutar. Dışarıya setter açmaz; değişiklik yalnız dispatcher'dan gelen action ile olur, sonra bir change event yayınlar.
- View: store'u dinler, state'i okur, ekrana çizer. Kullanıcı bir şey yaptığında state'i değiştirmez, action gönderir.
Buradaki kilit nokta inversion of control. MVC'de controller ya da başka bir model, thread.setAsRead() gibi bir method'la store'un içine uzanabiliyordu. Flux'ta store'un böyle bir method'u yok; kendi state'ini yalnız kendisi değiştiriyor. Store'lar arasındaki sıra da dispatcher'ın waitFor() method'uyla açıkça yazılıyor: okunmamış sayısı store'u, thread store'unun işini bitirmesini bekleyebiliyor. Önceki örnekteki "subscribe sırası sonucu değiştiriyor" problemi böylece kodda görünür bir bağımlılığa dönüşüyor.
Facebook'un Dispatcher implementation'ında bir kural daha var: bir action dağıtılırken yeni bir dispatch başlatılırsa Cannot dispatch in the middle of a dispatch. hatası fırlatılıyor. Cascading update'i mimari seviyede yasaklayan satır bu. Bir action'ın sonucu başka bir action gerektiriyorsa, ilki tamamen bittikten sonra ikincisi başlar; her güncelleme tek bir turda olur. Design Patterns serisinden tanıdık gelecek: dispatcher bir Mediator, action ise GoF (Gang of Four: Gamma, Helm, Johnson ve Vlissides'in 1994 tarihli Design Patterns kitabı) Command pattern'ındaki gibi isteği object'e çevirmenin bir hâli. Flux'ın orijinal kütüphanesi bugün arşivlenmiş durumda ama fikir her yerde yaşıyor.
Redux'un Üç Prensibi
2015'te Dan Abramov ve Andrew Clark, Flux'ı sadeleştiren Redux'u yayınladı. Redux dokümantasyonu yaklaşımı üç prensiple özetliyor:
| Prensip | Ne diyor | Flux'tan farkı |
|---|---|---|
| Single source of truth | Uygulamanın global state'i tek bir store'daki tek bir object ağacında durur. | Flux'ta alan başına birden çok store vardı. |
| State is read-only | State'i değiştirmenin tek yolu, ne olduğunu anlatan bir action göndermektir. | Aynı fikir; Redux bunu tek kurala indirir. |
| Changes are made with pure functions | Action'ın state'i nasıl dönüştürdüğünü pure reducer'lar tarif eder: (state, action) => newState. | Dispatcher ve waitFor() gereksizleşir; reducer'lar birleştirilir. |
Tek store ve pure reducer, Flux'taki store'lar arası sıralama problemini ortadan kaldırıyor. Bütün state tek bir fonksiyondan geçtiği için "önce hangisi güncellenir" sorusu, reducer'ı nasıl yazdığına indirgeniyor ve bu bir fonksiyonun içinde okunabilir bir kod. Pure olması da test edilebilirliği bedavaya getiriyor: reducer'ı test etmek için bir state ve bir action verip dönen değere bakman yeterli; DOM (Document Object Model), mock ve framework yok.
Elm Architecture: Model, Update, View
Redux'un ilham kaynaklarından biri Evan Czaplicki'nin Elm dili ve onun uygulama yapısı, The Elm Architecture. Elm rehberi bunu üç parçayla anlatıyor: Model uygulamanın state'i, View state'i HTML'e (HyperText Markup Language) çeviren fonksiyon, Update ise gelen mesaja göre state'i değiştiren fonksiyon. Kullanıcı bir butona bastığında view bir mesaj üretir, runtime bu mesajı update'e verir, yeni model view'a gider.
Elm'de bu akışın dışına çıkmak mümkün değil: değerler immutable, mutation yok, side effect'ler yalnız runtime'a bırakılan komutlarla (Elm'deki adıyla Cmd) yapılıyor. TypeScript'te aynı disiplini dil vermiyor, sen kurman gerekiyor. Aşağıdaki store bunu yapmaya çalışıyor: Elm'deki Msg'nin karşılığı action, update'in karşılığı reducer, view'ın karşılığı store'u dinleyen render fonksiyonu.
40 Satırlık Bir Store
export type Reducer<S, A> = (state: S, action: A) => S;
export type Listener = () => void;
export interface Store<S, A> {
getState(): S;
dispatch(action: A): void;
subscribe(listener: Listener): () => void;
}
export function createStore<S, A>(reducer: Reducer<S, A>, initialState: S): Store<S, A> {
let state = initialState;
let reducing = false;
const listeners = new Set<Listener>();
return {
getState: () => state,
dispatch(action) {
// Flux Dispatcher'daki kural: bir action işlenirken yenisi başlatılamaz
if (reducing) throw new Error(`Cannot dispatch while reducing: ${JSON.stringify(action)}`);
reducing = true;
let next: S;
try {
next = reducer(state, action);
} finally {
reducing = false;
}
// Referans değişmediyse hiçbir şey değişmemiştir
if (next === state) return;
state = next;
listeners.forEach(listener => listener());
},
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
};
}
Üç method var. getState o anki state'i verir, subscribe bir listener ekler ve aboneliği bitiren bir fonksiyon döner, dispatch reducer'ı çalıştırır ve listener'lara haber verir. İki küçük karar bu store'u Flux'ın derslerine bağlıyor. Birincisi, reducer çalışırken gelen dispatch'i reddetmek; Facebook'un Dispatcher'ındaki kuralın aynısı. İkincisi, reducer aynı referansı döndürürse kimseye haber vermemek. Bu ikincisi immutability'nin karşılığını aldığımız yer, birazdan döneceğiz.
Reducer'ın içinden yeni bir action başlatmaya çalışırsan sonuç:
import { createStore } from './store.js';
import { initialOrderState, orderReducer, type OrderAction, type OrderState } from './order.js';
// Kötü örnek: reducer içinden yeni bir action başlatmak
const store = createStore((state: OrderState, action: OrderAction): OrderState => {
const next = orderReducer(state, action);
if (action.type === 'bookSelected') store.dispatch({ type: 'quantityChanged', quantity: 2 });
return next;
}, initialOrderState);
store.dispatch({ type: 'bookSelected', book: { id: 1, title: 'The Hobbit', price: 14 } });
$ node out/nested.js
Error: Cannot dispatch while reducing: {"type":"quantityChanged","quantity":2}
Redux'ta da aynı kural var: reducer içinden dispatch hata fırlatır. Kitap seçilince adedin 2 olması gerekiyorsa bu bilgi reducer'ın kendisine yazılır, ikinci bir action'a değil.
Sipariş Formu Store'a Taşınıyor
Şimdi açılıştaki formu bu yapıya taşıyalım. State yalnız kaynak verileri tutuyor: seçili kitap, adet, gönderim durumu ve sipariş numarası. Ara toplam, indirim ve toplam state'te yok; her okumada state'ten hesaplanıyorlar.
export interface Book {
readonly id: number;
readonly title: string;
readonly price: number;
}
export interface OrderState {
readonly book: Book | null;
readonly quantity: number;
readonly status: 'editing' | 'submitting' | 'submitted';
readonly orderId: string | null;
}
// Action'lar "ne oldu"yu anlatır, setter değildir
export type OrderAction =
| { type: 'bookSelected'; book: Book }
| { type: 'quantityChanged'; quantity: number }
| { type: 'submitRequested' }
| { type: 'submitSucceeded'; orderId: string };
export const initialOrderState: OrderState = { book: null, quantity: 1, status: 'editing', orderId: null };
export function orderReducer(state: OrderState, action: OrderAction): OrderState {
switch (action.type) {
case 'bookSelected':
return { ...state, book: action.book, quantity: 1 };
case 'quantityChanged': {
const quantity = Math.min(Math.max(action.quantity, 1), 10);
// Değişiklik yoksa aynı referans döner, kimse yeniden render edilmez
return quantity === state.quantity ? state : { ...state, quantity };
}
case 'submitRequested':
return canSubmit(state) ? { ...state, status: 'submitting' } : state;
case 'submitSucceeded':
return { ...state, status: 'submitted', orderId: action.orderId };
default:
return assertNever(action);
}
}
// Derived değerler state'te tutulmaz, state'ten hesaplanır
export const selectSubtotal = (s: OrderState): number => (s.book ? s.book.price * s.quantity : 0);
export const selectDiscount = (s: OrderState): number => (s.quantity >= 3 ? selectSubtotal(s) / 10 : 0);
export const selectTotal = (s: OrderState): number => selectSubtotal(s) - selectDiscount(s);
export const canSubmit = (s: OrderState): boolean => s.book !== null && s.status === 'editing';
function assertNever(action: never): never {
throw new Error(`Unhandled action: ${JSON.stringify(action)}`);
}
Action adlarının geçmiş zaman olduğuna dikkat et: quantityChanged, bookSelected. Redux Style Guide bunu "action'ları setter olarak değil, event olarak modelle" diye öneriyor. setQuantity adlı bir action, reducer'ın işini çağırana taşır; quantityChanged ise yalnız ne olduğunu söyler, kuralları (1 ile 10 arası sınırlama, kitap değişince adedin sıfırlanması) reducer'a bırakır.
Form kullanımı ve gönderim akışı:
import { createStore } from './store.js';
import { initialOrderState, orderReducer, selectDiscount, selectTotal, type OrderState } from './order.js';
const store = createStore(orderReducer, initialOrderState);
// View: state'i okur, ekrana basar; state'i asla doğrudan değiştirmez
function renderOrderForm(state: OrderState): void {
console.log(
`render: ${state.book?.title ?? '-'} x${state.quantity} ` +
`total=${selectTotal(state).toFixed(2)} discount=${selectDiscount(state).toFixed(2)} status=${state.status}`,
);
}
store.subscribe(() => renderOrderForm(store.getState()));
// Side effect reducer'da değil, action'lar arasında yaşar
async function submitOrder(): Promise<void> {
store.dispatch({ type: 'submitRequested' });
if (store.getState().status !== 'submitting') return;
const orderId = await Promise.resolve('ORD-1001');
store.dispatch({ type: 'submitSucceeded', orderId });
}
store.dispatch({ type: 'bookSelected', book: { id: 1, title: 'The Hobbit', price: 14 } });
store.dispatch({ type: 'quantityChanged', quantity: 2 });
store.dispatch({ type: 'quantityChanged', quantity: 3 });
store.dispatch({ type: 'quantityChanged', quantity: 50 });
store.dispatch({ type: 'quantityChanged', quantity: 99 });
await submitOrder();
await submitOrder();
$ node out/main.js
render: The Hobbit x1 total=14.00 discount=0.00 status=editing
render: The Hobbit x2 total=28.00 discount=0.00 status=editing
render: The Hobbit x3 total=37.80 discount=4.20 status=editing
render: The Hobbit x10 total=126.00 discount=14.00 status=editing
render: The Hobbit x10 total=126.00 discount=14.00 status=submitting
render: The Hobbit x10 total=126.00 discount=14.00 status=submitted
Sekiz dispatch, altı render ve hiçbiri tutarsız değil. Adet 3 olduğu anda toplam ve indirim aynı satırda doğru görünüyor, çünkü ikisi de aynı state'ten tek seferde hesaplanıyor; arada çalışan bir listener sırası yok. Adet 99 istendiğinde reducer onu 10'a sınırlıyor, adet zaten 10 olduğu için aynı state'i döndürüyor ve render hiç çalışmıyor. İkinci submitOrder çağrısı da aynı şekilde sessizce bitiyor: sipariş gönderilmiş, canSubmit false.
Gönderim işinin nerede durduğuna bak. HTTP (Hypertext Transfer Protocol) isteği reducer'da değil, iki action arasında: önce submitRequested, cevap gelince submitSucceeded. Reducer pure kalıyor, side effect onun dışında. Redux'ta bu iş middleware'e ve thunk'lara, NgRx'te effect'lere, Elm'de runtime'a bırakılan komutlara düşüyor. İsimler farklı, kural aynı.
TypeScript'in discriminated union'ı burada ayrıca iş görüyor. default dalındaki assertNever, her action türünün ele alındığını derleyiciye kontrol ettiriyor. Union'a { type: 'couponApplied'; code: string } ekleyip reducer'a dokunmazsan:
$ npx tsc -p .
src/order.ts(38,32): error TS2345: Argument of type '{ type: "couponApplied"; code: string; }' is not assignable to parameter of type 'never'.
Yeni bir action eklemek, onu işlemeyi unuttuğun her reducer'da derleme hatası demek. Büyük bir ekipte bu, "bu action'ı kim işliyor?" sorusunun cevabını derleyiciye devretmek anlamına geliyor.
Angular'a Kısa Bir Köprü
Bu store hiçbir framework'e bağlı değil. Angular 22'de bir component'e bağlamak için store'u bir signal'a köprülemek yetiyor:
import { Component, DestroyRef, InjectionToken, computed, inject, signal } from '@angular/core';
import { createStore, type Store } from './store';
import { initialOrderState, orderReducer, selectTotal, type OrderAction, type OrderState } from './order';
export const ORDER_STORE = new InjectionToken<Store<OrderState, OrderAction>>('ORDER_STORE', {
providedIn: 'root',
factory: () => createStore(orderReducer, initialOrderState),
});
@Component({
selector: 'app-order-summary',
template: `
<p>Total: {{ total() }}</p>
<button (click)="addOne()">+1</button>
`,
})
export class OrderSummary {
private readonly store = inject(ORDER_STORE);
private readonly state = signal(this.store.getState());
protected readonly total = computed(() => selectTotal(this.state()));
constructor() {
// Store değişince signal güncellenir, OnPush component yeniden çizilir
const unsubscribe = this.store.subscribe(() => this.state.set(this.store.getState()));
inject(DestroyRef).onDestroy(unsubscribe);
}
protected addOne(): void {
this.store.dispatch({ type: 'quantityChanged', quantity: this.state().quantity + 1 });
}
}
Store değişince signal güncelleniyor, computed toplamı yeniden hesaplıyor ve Angular 22'de varsayılan olan OnPush component yeniden çiziliyor. Butona basmak state'i değiştirmiyor, bir action gönderiyor. Bu bağlamanın Vitest testi Total: 28 sonucunu doğruluyor. Bir sonraki yazıda bu köprüye bile ihtiyaç duymadan, signal'ların kendisiyle aynı akışı kuracağız.
Immutability'nin Rolü
Reducer'ın her seferinde yeni bir object döndürmesi ilk bakışta israf gibi görünür. Ama karşılığında çok ucuz bir soru kazanıyorsun: "bir şey değişti mi?" sorusu next === state karşılaştırmasına iniyor. State'i yerinde değiştirseydin bu soruyu cevaplamak için object'in her property'sini, iç içe her array'i gezmen gerekirdi. AngularJS'in digest loop'u tam olarak bunu yapıyordu.
Aynı fikir Angular'da da karşına çıkıyor. OnPush change detection, input'a gelen değerin referansı değişmediyse component'i kontrol etmiyor; Angular 22'de OnPush varsayılan olduğu için array'e push yapıp ekranın güncellenmesini beklemek artık çalışmıyor. Immutability burada bir zevk meselesi değil, değişikliği görünür kılmanın yolu.
State type'larını readonly property'lerle tanımlamak, yanlışlıkla yapılan mutation'ı derleme anında yakalıyor:
import type { OrderAction, OrderState } from './order.js';
// Kötü örnek: state'i yerinde değiştirmek
export function badReducer(state: OrderState, action: OrderAction): OrderState {
if (action.type === 'quantityChanged') state.quantity = action.quantity;
return state;
}
$ npx tsc -p .
src/bad-reducer.ts(5,50): error TS2540: Cannot assign to 'quantity' because it is a read-only property.
Bu reducer derlenseydi iki bug birden üretirdi: aynı referansı döndürdüğü için store hiçbir listener'a haber vermez, ekran eski kalır; üstelik önceki state'in kendisi değiştiği için "önceki" ve "sonraki" state'i karşılaştıran her araç (undo, time-travel debugging) yanlış sonuç verir. readonly derleme anında koruma sağlıyor, runtime'da değil. Redux Toolkit bu yüzden geliştirme modunda mutation'ları yakalayan bir middleware ekliyor ve reducer'ı, mutation'a benzeyen ama arka planda yeni object üreten Immer ile yazdırıyor; Immer ürettiği state'i varsayılan olarak donduruyor.
Boilerplate'in Bedeli ve Ne Zaman Gerekmez
Bu yaklaşımın bir faturası var ve görmezden gelmek dürüst olmaz. Bir input'a yazılan değeri ekranda göstermek için bir action type, bir union üyesi, reducer'da bir case ve bir selector yazıyorsun. Klasik Redux'ta buna action creator'lar, action type sabitleri ve ayrı dosyalar ekleniyordu. Dan Abramov'un 2016'daki "You Might Not Need Redux" yazısı tam da bu yüzden yazıldı: bu kısıtları, karşılığında aldığın şeylere (tek yerden izlenebilir değişiklik, kaydedilip tekrar oynatılabilir action'lar, pure reducer testleri) ihtiyacın varsa kabul et.
Sipariş formunun tek başına bir store'a ihtiyacı yok. Bir component'in kendi içinde kalan state'i (açık mı kapalı mı bir dropdown, henüz gönderilmemiş bir input) global bir store'a taşınınca kazanç sıfır, maliyet ise her değişiklikte dört dosya. Store'un kazandırdığı yer, aynı state'i birden çok uzak component'in okuyup değiştirdiği yer: sepet sayacı header'da, sepet içeriği yan panelde, stok uyarısı ürün kartında. Açılıştaki cascading update problemi tam olarak bu durumda ortaya çıkıyordu.
Ayrıca unidirectional data flow ile Redux aynı şey değil. Flow bir prensip: state'in tek bir sahibi var, değişiklikler tek kapıdan giriyor, türetilmiş değerler state'ten hesaplanıyor. Redux bu prensibin bir implementation'ı. Angular'da aynı prensibi signal'lar ve bir service ile, store kütüphanesi olmadan uygulamak mümkün; bir sonraki yazının konusu bu.
Sık Düşülen Tuzaklar
Derived state'i state'e yazmak. total'ı state'te tutup her action'da güncellemeye çalışmak, açılıştaki glitch'i reducer'ın içine taşımaktır. Bir action quantity'yi güncelleyip total'ı unutursa iki değer birbirinden kopar. Kaynak veriyi tut, gerisini selector'la hesapla.
Reducer'da side effect. Reducer'ın içinde HTTP isteği atmak, Date.now() okumak ya da localStorage'a yazmak onu pure olmaktan çıkarır; aynı state ve action ile iki kez çağrıldığında farklı sonuç verir. Zamana ya da rastgeleliğe ihtiyacın varsa değeri action'ın içinde getir.
Action'ları setter gibi adlandırmak. setTotal, setStatus gibi action'lar store'u süslü bir değişken setine çevirir; kurallar çağıran tarafa dağılır. Action, "kullanıcı adedi değiştirdi" gibi bir olayı anlatmalı.
Her şeyi tek store'a koymak. Single source of truth "her state global olmalı" demek değil. Bir modal'ın açık olup olmadığını global store'da tutmak, o modal'ı kullanan her yeri store'a bağımlı hâle getirir. State'i, onu okuyan en yakın ortak sahibin yanına koy.
Store'u dinlerken aboneliği bırakmamak. subscribe'ın döndürdüğü fonksiyonu çağırmazsan, yok olan bir component'in listener'ı store'da yaşamaya devam eder. Angular örneğinde DestroyRef.onDestroy bu yüzden var.
Küçük Bir Deneme
★☆☆ cascade.ts'te discount listener'ını kaldır ve indirimi total listener'ının içinde hesapla. Kaç render oluyor? Sonra aynı kuralı store'daki selectDiscount ile karşılaştır: hangisinde kuralın tek bir yerde durduğundan emin olabiliyorsun?
★★☆ Store'a kupon desteği ekle: couponApplied action'ı, state'te couponCode, WELCOME5 kodu için toplamdan 5 düşen bir kural. Önce union'ı genişlet ve derleyicinin seni nereye yönlendirdiğine bak; indirimi state'e değil selector'a yaz. Yüzde 10 indirim ile kupon birlikte uygulanırken sıra ne olmalı, bu kararı nereye yazdın?
★★★ createStore'a dispatch edilen her action'ı ve sonrasındaki state'i bir array'e kaydeden küçük bir "history" ekle. Kayıtlı action'ları initialOrderState'ten başlayarak reducer'dan tekrar geçirip son state'i yeniden üret. Reducer'ın içine Date.now() koyduğunda bu tekrar oynatma neden bozuluyor?
Bu yazıda unidirectional data flow'u framework'süz, en çıplak hâliyle kurduk: action, reducer, store ve onu dinleyen view. Bir sonraki yazıda aynı akışı Angular'ın kendi araçlarıyla kuruyoruz: veri parent'tan child'a input() ile iner, olaylar output() ile yukarı çıkar, derived state computed() ile hesaplanır ve state, signal tutan bir service'te yaşar.