Bölüm 9 / 16
Durum yönetimi
Zustand, Redux Toolkit; ne zaman gerekir.
11 dakikalık okuma
10. hattın 12. dersinde verilen söz burada ödeniyor: NgRx'i "Redux'un Angular uyarlaması" diye tanıtmıştık — bu ders Redux'un kendi evine, React'e döner. Ama kütüphaneden önce karar gelir: karar merdiveni (local → lifted → context → store), hangi belirtinin hangi basamağa çıkardığını söyler; store en üst basamaktır, varsayılan değil. Sonra iki aracı tanıyorsun: Zustand (selector'lı, törensiz küçük store) ve Redux Toolkit (createSlice, immer, DevTools — NgRx ile aynı desenin aslı). Kapanış ilkesi 10. hattakiyle aynıdır: her şey store'a girmez — sunucu verisi 10. derse, ekran adresi 11. derse gider.
01Ne öğreneceksin
- Karar merdiveni: local → lifted → context → store — hangi belirti hangi basamağa çıkarır
- Zustand:
create, eylem fonksiyonları, selector'la seçici abonelik (context'in eksiği burada kapanır) - Redux Toolkit:
configureStore+createSlice; immer'in değişmezlik sözleşmesiyle bağı - NgRx köprüsü: aynı desen, iki uyarlama — 10. hattın Redux kıyası sözü ödenir
- "Her şey store'a girmez": form taslağı, sunucu verisi, URL durumu nereye gider; RTK Query'nin adı
- Araç seçimi: hangi projede Zustand, hangisinde Redux Toolkit, hangisinde hiçbiri
02Karar merdiveni: store bir varış değil, son basamak
10. hattın 12. dersindeki merdivenin React hâli. Her basamağın bir belirtisi vardır; belirti yokken basamak çıkmak, mimariyi süs için taşımaktır:
| Basamak | Belirti | Araç |
|---|---|---|
| Yerel | Durum tek bileşenin derdi | useState / useReducer (2. ve 6. ders) |
| Yukarı taşınmış | İki kardeş aynı veriyi görmeli | Lifting + tek doğruluk kaynağı (2. ders) |
| Context | Ağaç geneli, az değişen değer; drilling bileşimle çözülemedi | Context + reducer (8. ders) |
| Store | Çok bileşen + sık güncelleme + seçici abonelik ihtiyacı; geçmiş/araç izleme (DevTools) isteği; context zincirleri dolaşmaya başladı | Zustand / Redux Toolkit (bu ders) |
Somut çeviri: başvuru formunun taslağı yerel kalır; filtre, listeyle paylaşılmak için yukarı taşınır; oturum ve tema context'tedir; "birçok ekranın okuyup yazdığı, sık değişen, geçmişi izlenmek istenen" istemci durumu — çok adımlı bir sihirbazın ilerleyişi, uygulama geneli bildirim kuyruğu, karmaşık bir düzenleyicinin durumu — store'a çıkar. Merdivenin altın kuralı 10. hattakiyle aynıdır: bir üst basamağa ancak alt basamağın acısı görülünce çıkılır; "ileride lazım olur" diye kurulmuş store, projeye maliyetini ilk günden ödetir.
03Zustand: törensiz store
Zustand, "store" fikrinin en küçük dürüst hâlidir: modül seviyesinde bir
create çağrısı, state + eylemleri tek nesnede toplar; Provider yoktur,
bileşen store'a bir hook'la bağlanır:
import { create } from "zustand";
type BildirimDurumu = {
bildirimler: Bildirim[];
seciliId: number | null;
bildirimGeldi: (b: Bildirim) => void;
secimDegisti: (id: number | null) => void;
};
export const useBildirimStore = create<BildirimDurumu>()((set) => ({
bildirimler: [],
seciliId: null,
bildirimGeldi: (b) =>
set((d) => ({ bildirimler: [...d.bildirimler, b] })), // değişmezlik bizde (2. ders)
secimDegisti: (id) => set({ seciliId: id }), // kısmi nesne: geri kalanı korunur
}));
Asıl olay bileşen tarafındadır — selector:
// yalnız SAYIYI seçiyorum: bildirim listesi değişmedikçe render YOK
const sayi = useBildirimStore((d) => d.bildirimler.length);
// yalnız eylemi seçiyorum: eylem referansı sabittir → bu bileşen HİÇ render olmaz
const bildirimGeldi = useBildirimStore((d) => d.bildirimGeldi);
Selector, 8. derste context'in veremediği şeyi verir: store değiştiğinde herkes
değil, seçtiği dilim Object.is ile değişenler render olur —
abonelik köprüsü, 7. derste tanıdığın useSyncExternalStore ile
kuruludur. İki kıdem notu: selector'dan render içinde yeni nesne türetme
(d => d.bildirimler.filter(...) her seferinde yeni referans döndürür
ve her yayında render yersin — türetmeyi useMemo'ya ya da store'a al);
ve store modül seviyesi bir tekil olduğu için sunucu tarafında render'da
(12. hatta göreceğin SSR) istekler arası sızıntı riski taşır — Zustand'ın SSR
kılavuzu bu yüzden store'u istek başına kurdurur. Küçük-orta ölçekli istemci
durumu için bugün topluluğun varsayılanı budur.
04Redux Toolkit: desenin aslı, modern ambalajda
Redux, 10. hattın 12. dersinde NgRx üzerinden öğrendiğin desenin kendisidir: tek
store, eylemler (olay, emir değil), saf reducer'lar, tek yönlü akış. Modern Redux
Redux Toolkit (RTK) demektir — eski el yazması eylem sabitleri ve switch
törenleri yerine createSlice:
import { configureStore, createSlice } from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit";
const basvuruSlice = createSlice({
name: "basvurular",
initialState: { liste: [] as Basvuru[], seciliId: null as number | null },
reducers: {
basvuruEklendi(d, e: PayloadAction<Basvuru>) {
d.liste.push(e.payload); // "mutasyon" — immer taslağı üzerinde, MEŞRU
},
basvuruSilindi(d, e: PayloadAction<number>) {
d.liste = d.liste.filter(b => b.id !== e.payload);
if (d.seciliId === e.payload) d.seciliId = null; // tutarlılık tek yerde (6. ders)
},
},
});
export const { basvuruEklendi, basvuruSilindi } = basvuruSlice.actions;
// eylem üreticileri OTOMATİK: basvuruSilindi(5) → { type: "basvurular/basvuruSilindi", payload: 5 }
export const store = configureStore({
reducer: { basvurular: basvuruSlice.reducer },
});
// bileşen tarafı: <Provider store={store}> + useSelector / useDispatch
Buradaki push satırı 2. dersin Object.is sözleşmesine
ihanet gibi görünür — değildir: createSlice, reducer'ını immer
ile sarar. Yazdığın "mutasyon" gerçek nesneye değil bir taslağa işler; immer
taslaktan, değişen dallar için yeni referanslar taşıyan yeni bir değişmez
nesne üretir. Sözleşme değişmedi, spread zinciri yazma yükü kütüphaneye devredildi
— iç içe yapılarda hata kaynağı olan { ...d, ic: { ...d.ic } }
merdivenleri kalktı. Bedeli bilinçtir: taslak dışına taslak referansı sızdırmak ve
immer'siz bağlamda aynı stili sürdürmek klasik hatalardır.
NgRx köprüsünü tek tabloda kapatalım — 10-12'de "Redux kıyası React'te gelecek"
demiştik, işte karşılıklar: NgRx Action ↔ slice eylemi;
createReducer ↔ createSlice.reducers; memoized
createSelector ↔ RTK'nın da içerdiği aynı reselect
kütüphanesi; NgRx Effects ↔ RTK'da thunk/listener ara katmanları
(asenkron işin reducer dışına alınması — aynı ilke). Redux DevTools iki dünyada da
aynı eklentidir: eylem akışı, durum farkları, zamanda geri sarma. Desen bir,
uyarlama iki — mülakatta bu cümle iki hattın da hakkını verir.
05Her şey store'a girmez
10. hattın 12. dersindeki ilkenin React hâli — store kurulduğunda ilk savrulma, her veriyi içine süpürmektir. Üç sınır çizgisi:
- Form taslağı ve geçici UI durumu (açık panel, imleç, taslak metin)
yereldir — store'a taşımak her tuşu uygulama geneli bir olaya çevirir, render
yayılımını ve gürültüyü büyütür. 4. dersin controlled formu
useState'iyle mutludur. - Sunucudan gelen veri store'un en büyük yanlış kiracısıdır. Başvuru listesi senin durumun değil, sunucunun durumunun kopyasıdır: bayatlar, yeniden çekilir, önbelleklenir, sayfalanır — bu yaşam döngüsünü elle store'da yönetmek, 10. derste göreceğin TanStack Query'nin işini elle yapmaktır. RTK'nın kendi cevabının adını bil: RTK Query — Redux dünyasında aynı işi gören veri katmanı; Redux'lu projede onu, değilse TanStack Query'yi seçersin.
- Ekran adresi olan durum (seçili sekme, filtre, sayfa numarası) URL'de yaşamalıdır — paylaşılabilir, yer imlenebilir, geri tuşuyla gezilebilir. Bu 11. dersin konusudur; store'a koyarsan kullanıcı linki paylaştığında durum kaybolur.
Süzgeçten geçince store'da kalan şey çoğu projede şaşırtıcı kadar azdır: oturum benzeri kimlik bilgisi (context de yeter), uygulama geneli bildirimler, çok adımlı akış ilerleyişi, gerçek zamanlı beslemelerin istemci tamponu. "Store'umuz küçükse doğru yoldayız" — iki hattın ortak kıdem cümlesi.
06Seçim: hangi projede hangisi?
Karar üç soruyla verilir. (1) Store'luk belirti var mı? Yoksa merdivenin alt basamakları yeter — React projelerinin önemli bölümü context + Query ikilisiyle store'suz yaşar. (2) Varsa ekip ve ölçek ne istiyor? Küçük-orta istemci durumu, az tören, hızlı başlangıç → Zustand. Büyük ekip, katı konvansiyon ihtiyacı, zengin ara katman/araç ekosistemi, mevcut Redux bilgisi → Redux Toolkit. (3) Sunucu verisi kim? Cevap her iki durumda da aynı: store değil, veri katmanı (TanStack Query / RTK Query). Mülakat çerçevesi de budur: araç adı saymak değil, belirti → basamak → araç zincirini gerekçeyle yürümek; "Redux bilmek", eylem-reducer-selector desenini bilmektir ve o desen sende iki uyarlamasıyla (NgRx + RTK) var.
07Mini alıştırma — Merdiveni tırman
Yaklaşık 20 dakika · StackBlitz projene zustand ve @reduxjs/toolkit + react-redux paketlerini ekle
- Bildirim kuyruğunu Zustand'la kur (bu dersteki store); üstbara yalnız
bildirimler.length'i seçen bir sayaç, ayrı bir panele listeyi bağla. Profiler'da kanıtla: seçim değişince sayaç render olmuyor, liste değişince ikisi de oluyor. - Selector tuzağını yaşa: sayacın selector'ını bilerek
d => d.bildirimler.filter(b => !b.okundu)yap; her yayında render yediğini gör, sebebini (her çağrıda yeni dizi referansı) yaz ve düzelt. - Aynı bildirim durumunu bu kez RTK ile kur:
createSlice+configureStore+useSelector/useDispatch. Reducer'dapushkullan ve dışarıdan gelen state referansının yine de her eylemde değiştiğiniObject.iskıyasıyla konsolda kanıtla — immer'in işini gör. - "Silinen seçiliyse seçimi düşür" kuralını slice'a taşı (6. dersin reducer alıştırmasının store ölçeği); Redux DevTools eklentisiyle eylem akışını izle, bir eylemi geri sar.
- Reducer'ı bileşensiz test et:
basvuruSlice.reducer'ı iki eylemle elle çağır, dönen state'leri doğrula — NgRx'te yaptığın testin aynısı olduğunu not et. - Sınır çizgisi antrenmanı: projendeki beş veriyi (form taslağı, oturum, başvuru listesi, seçili sekme, bildirimler) merdiven + üç sınır kuralıyla yerleştir; her biri için tek cümle gerekçe yaz.
08Kendini sına
Karar merdivenini belirtileriyle anlat: hangi durum hangi basamakta yaşar ve "erken store" neden bir tasarım hatasıdır?
Merdiven dört basamaktır ve her birine bir belirti
çıkarır. Durum tek bileşenin derdiyse yereldir: useState, geçişler
karmaşıksa useReducer. İki kardeş aynı veriyi görmeliyse en yakın
ortak ataya taşınır — lifting + tek doğruluk kaynağı. Değer ağacın genelinden
okunuyor ve az değişiyorsa (oturum, tema, dil), drilling de bileşimle
çözülemiyorsa context + reducer kalıbı. Çok bileşenin okuyup yazdığı, sık
güncellenen, seçici abonelik ve araç izleme (DevTools, geçmiş) isteyen istemci
durumu ise store basamağıdır — Zustand ya da Redux Toolkit. Kural, bir üst
basamağa ancak alttakinin acısı görülünce çıkmaktır. Erken store iki bedel
ödetir: her küçük durum değişikliği uygulama geneli tören (eylem, reducer,
selector) ister ve store, form taslağı gibi yerel verileri de içine çekerek hem
gürültüyü hem render yayılımını büyütür. Projelerin önemli bölümü context +
sunucu durumu katmanıyla store'suz yaşar; store'un küçük kalması sağlık
işaretidir.
Zustand'ın selector mekanizması context'in hangi eksiğini kapatır? İki selector tuzağını ve store'un tekil doğasının riskini anlat.
Context'te Provider value'su değişince o context'i okuyan
tüm bileşenler render olur — seçici abonelik yoktur. Zustand'da bileşen store'a
selector'la bağlanır: useStore(d => d.dilim); store her
değiştiğinde selector çalışır ama bileşen yalnız kendi seçtiği dilim
Object.is ile değiştiyse render olur (köprü
useSyncExternalStore ile kurulur). Eylemi seçen bileşen hiç render
olmaz çünkü eylem fonksiyonlarının referansı sabittir. Birinci tuzak: selector
içinde her çağrıda yeni referans türetmek — filter/map
sonucu her yayında "değişti" sayılır ve seçicilik kaybolur; türetme
useMemo'ya ya da store'daki hazır bir alana alınır. İkinci tuzak:
değişmezlik sorumluluğu sende kalır — set'e verdiğin güncellemede
yeni referanslar üretmek zorundasın. Risk: store modül seviyesinde bir tekildir;
tarayıcıda sorun değilken sunucu tarafı render'da (SSR) istekler arası durum
sızıntısı yaratabilir — SSR'da store istek başına kurulur.
Redux Toolkit'te createSlice reducer'ında d.liste.push(...) yazmak neden meşrudur? İmmer'in mekanizmasını ve değişmezlik sözleşmesiyle ilişkisini anlat.
Çünkü o satır gerçek state'e değil immer'in taslağına
işler. createSlice her reducer'ı immer ile sarar: reducer'a gelen
d bir Proxy taslaktır; üzerinde yapılan "mutasyonlar" kaydedilir ve
immer, reducer bitince değişen dallar için yeni referanslar taşıyan yepyeni bir
değişmez nesne üretir — değişmeyen dallar eski referanslarını korur (yapısal
paylaşım). Dışarıdan bakan dünya için Object.is sözleşmesi aynen
geçerlidir: her eylem, değişen yolda yeni referans demektir; selector'lar ve
React'in render kıyasları bozulmaz. Kazanç, iç içe yapılarda hataya açık spread
merdivenlerinin ({ ...d, ic: { ...d.ic, alan: x } }) kalkmasıdır.
Dikkat isteyen iki nokta: taslak referansını reducer dışına sızdırmamak (taslak,
reducer bitince geçersizdir) ve bu yazım stilinin yalnız immer'li bağlamda meşru
olduğunu bilmek — sıradan useState güncellemesinde push
hâlâ 2. dersin ihlalidir.
"Sunucudan gelen liste Redux store'unda dursun" diyen ekip kararına itirazını yap; NgRx-RTK köprüsünü ve RTK Query'nin yerini de bağla.
İtiraz sınıflandırmadan başlar: başvuru listesi istemci durumu değil, sunucu durumunun kopyasıdır — sahibi sunucudur, bayatlar, yeniden çekilmek, önbelleklenmek, sayfalanmak ve istekler arasında paylaşılmak ister. Bu yaşam döngüsünü store'da elle kurmak (yükleniyor/hata bayrakları, önbellek anahtarları, yeniden getirme zamanlaması) özel araçların işini el emeğiyle yapmaktır ve store'u asıl işinden uzaklaştırır. Doğru adres bir veri katmanıdır: bağımsız projede TanStack Query; Redux'lu projede Redux ekosisteminin kendi cevabı RTK Query — önbellek, geçersiz kılma ve otomatik hook üretimiyle aynı işi store'un yanında görür. Köprü cümlesi: Redux ve NgRx aynı desenin iki uyarlamasıdır — eylem = olay, saf reducer, memoized selector (ikisinde de reselect fikri), asenkron işin reducer dışına alınması (Effects ↔ thunk/listener) ve aynı Redux DevTools. Bu yüzden karar "Redux'a mı NgRx'e mi yatırım" değil, desene yatırımdır; sunucu verisi ise her iki dünyada da store'un değil veri katmanının kiracısıdır.
09Sınav köşesi
Yazılıda nasıl sorulur
(1) Durum yönetimi karar merdiveninin basamakları nelerdir? (2) Hangi
belirtiler store ihtiyacını gösterir? (3) Zustand'da selector ne işe yarar;
bileşen ne zaman render olur? (4) Zustand ile Context API'nin render davranışı
farkı nedir? (5) Redux'un üç temel ilkesi nedir (tek store, salt okunur state,
saf reducer)? (6) createSlice ne üretir; eylem tipleri nereden
gelir? (7) İmmer ne yapar; değişmezlik sözleşmesi neden bozulmaz? (8) NgRx ile
Redux Toolkit'in ortak deseni ve karşılıkları nelerdir? (9) Sunucu verisi neden
store'a konmaz; RTK Query nedir? (10) Hangi durum URL'de yaşamalıdır?
Sözlüde nasıl anlatılır — 2 dakikalık taslak
Düzen: (1) Merdiven — "Durum yönetimini merdivenle anlatırım: yerel → yukarı taşınmış → context → store; üst basamağa ancak alttakinin acısı görülünce çıkarım, store varsayılan değil son basamaktır." (2) Zustand — "Törensiz store: Provider yok, bileşen selector'la bağlanır ve yalnız seçtiği dilim değişince render olur — context'in seçici abonelik eksiğini kapatır." (3) RTK — "Modern Redux, Toolkit demektir: createSlice eylem üreticilerini otomatik çıkarır, immer sayesinde taslak üzerinde mutasyon yazarım ama dışarıya değişmez yeni referanslar çıkar — Object.is sözleşmesi korunur." (4) Köprü — "NgRx'te öğrendiğim desenin aslı: eylem = olay, saf reducer, memoized selector, aynı DevTools; desen bir, uyarlama iki." (5) Kıdem vuruşu — "Her şey store'a girmez: form taslağı yerel, sunucu verisi Query/RTK Query'de, ekran adresi URL'de — store küçükse doğru yoldayız."
