Bölüm 7 / 16
Custom hook yazmak
Yeniden kullanılabilir mantık.
12 dakikalık okuma
6. ders kapıyı araladı: tekrarlanan state + effect örgüsü
use önekli bir fonksiyona çıkar ve custom hook state değil mantık
paylaşır. Bu ders o kapıdan içeri girer: hook'un tasarım sözleşmesi (girdiler
neden "canlı"dır, dönüş dizi mi nesne mi olmalı, dışa verilen fonksiyonlar neden
sabitlenir), üç gerçek örnek (useGecikmisDeger'in derinleşmesi,
useLocalStorage, useOnlineStatus +
useSyncExternalStore) ve iki disiplin: erken soyutlamama (4.
hattın üç tekrar ölçüsü) ile test edilebilir tasarım. Angular'da bu işin
sahibi servisler ve direktiflerdi; React'te "paylaşılan davranış"ın birimi hook'tur.
01Ne öğreneceksin
- Hook sözleşmesi: parametreler neden reaktif girdidir, hook her render'da neden yeniden çağrılır
- Dönüş biçimi kararı: dizi mi nesne mi —
as constve yeniden adlandırma serbestliği - Dışa verilen API'nin referans kararlılığı:
useCallback/useMemo'nun üçüncü meşru bağlamı iş başında useGecikmisDegerderinleşmesi +useLocalStorage+useOnlineStatus(useSyncExternalStoreile)- Erken soyutlama uyarısı: üç tekrar ölçüsü, "yanlış soyutlama tekrardan pahalıdır"
- Test edilebilirlik: saf mantığı hook'tan ayırmak;
renderHookfikri (derinliği 15. hatta)
02Sözleşme: hook sıradan bir fonksiyon değildir
Teknik olarak custom hook, içinde başka hook çağıran bir JavaScript fonksiyonudur — ama çağrılma biçimi ona sıradan fonksiyonlarda olmayan bir özellik verir: bileşenin her render'ında baştan çalışır (2. dersin "her render yeni çağrı" modeli hook'un içine de uzanır). Bunun iki sonucu sözleşmenin temelidir:
- Parametreler canlıdır.
useGecikmisDeger(filtre, 300)yazdığında hook'a "filtre'nin bugünkü değeri"ni bir kez vermezsin; her render'da güncel değeri yeniden verirsin. Hook'un içindeki effect, bağımlılık beyanıyla (3. ders) bu değişime tepki verir. Bu yüzden hook parametreleri "yapılandırma sabiti" değil reaktif girdi olarak tasarlanır — değişebilecek her şey parametredir, hook içine gömülü sihirli sabit bırakılmaz. - Her çağrı bağımsız bir örnektir. Aynı hook'u iki bileşende — ya da aynı bileşende iki kez — çağırmak, iki bağımsız state seti kurar (6. dersin "state değil mantık paylaşır" düzeltmesi). Hücre defteri (2. ders) bileşene aittir; hook o deftere satır ekler, defteri başka bileşenle paylaşmaz.
İsimlendirme de sözleşmenin parçasıdır: use öneki süs değil lint
anlaşmasıdır — hook kurallarının (koşulsuz, sıralı çağrı) denetimi bu öneke
bakar. Ad, gerçekleştirimi değil amacı söyler: useSetTimeoutWrapper
değil useGecikmisDeger; yarın gerçekleştirim değişse de ad doğru kalır
(4. hattın isimlendirme ilkesi).
03Dönüş biçimi: dizi mi, nesne mi?
Hook'un dışa verdiği şey onun API'sidir ve iki yerleşik biçim vardır. Karar ölçüsü topluluğun oturmuş pratiğidir:
| Biçim | Ne zaman | Neden |
|---|---|---|
Dizi (tuple) — [deger, setDeger] | İki elemanlı değer + eylem ikilisi | Çağıran yeniden adlandırabilir: const [taslak, setTaslak] = useLocalStorage(...) — aynı hook iki kez farklı adlarla kullanılır (useState'in kendi deseni) |
Nesne — { data, hata, yenile } | Üç ve üzeri alan, ya da alanların bir kısmı çoğu çağıranda kullanılmayacaksa | Sıra ezberi yok, seçerek alınır: const { yenile } = useBasvurular(); alan eklemek mevcut çağıranları kırmaz |
TypeScript tarafında dizi dönüşün bir tuzağı vardır: return [deger,
setDeger] ifadesinin çıkarımı tuple değil dizidir
((T | Setter)[]) ve çağıran tarafta tipler bulanır. Çözüm 9. hattan
tanıdığın as const: dönüş satırına eklenir, tip
readonly [T, Setter] olur.
İkinci ve mülakatta ayırt eden kural: hook'un dışa verdiği fonksiyonlar ve
nesneler referans kararlı olmalıdır. 6. derste useCallback'in üç
meşru bağlamından biri "custom hook'un dışarı verdiği API" idi — işte o bağlam.
Çağıran senin verdiğin yenile fonksiyonunu bir effect bağımlılığına
koyabilir; her render'da yeni referans dönersen çağıranın effect'i her render'da
çalışır ve kabahat senin hook'undadır. Kural: hook içinde tanımlanıp dışa
verilen her fonksiyon useCallback'e, dışa verilen derlenmiş nesne
useMemo'ya alınır. Bileşen içindeki "ölçmeden sarmalama" disiplini
burada tersine döner çünkü hook, çağıranının ne yapacağını bilemez — kütüphane
yazarı gibi savunmacı davranır.
04useGecikmisDeger: 6. dersin örneği derinleşiyor
6. dersteki hâli yeterli görünüyordu; kıdemli göz üç soruyu sorar. Önce kod:
function useGecikmisDeger<T>(deger: T, ms: number): T {
const [gecikmis, setGecikmis] = useState(deger); // ilk render: gecikmeden, aynen
useEffect(() => {
const id = setTimeout(() => setGecikmis(deger), ms);
return () => clearTimeout(id); // kur-yık: eski sayaç iptal
}, [deger, ms]);
return gecikmis;
}
- İlk render'da ne döner?
useState(deger)sayesinde ilk değer gecikmesiz görünür — boş ekranla başlamaz. "İlk değer de ms sonra gelsin" isteseydin başlangıcı ayrı tasarlardın; bu bir tasarım kararıdır ve hook'un dokümantasyon cümlesinde yazmalıdır. msdeğişirse? Bağımlılıkta olduğu için eski sayaç yıkılır, yenisi güncel süreyle kurulur — bekleme baştan başlar. Bağımlılıktan çıkarıp lint'i sustursaydın (3. dersin yasağı) hook, çağıranına sessizce yalan söylerdi.- Bu debounce mu? Davranış olarak evet: değer sustuğunda son hâli geçer. Kardeşi throttle ("en fazla saniyede bir geçir") farklı bir sözleşmedir; ikisini tek hook'a opsiyon bayrağıyla sıkıştırmak yerine iki ayrı ada ayırmak (KISS, 4. hat) okunurluğu korur.
05useLocalStorage: dış dünyayla ilk sözleşme
Sitenin kendisinin de kullandığı deseni hook'a çıkaralım: bir state,
localStorage ile senkron kalsın. Dış sistem devrede olduğu için (3.
dersin tanımı gereği) effect meşru; ama dış sistem güvenilmezdir ve kıdemli
kod bunu bilerek yazılır:
function useLocalStorage<T>(anahtar: string, varsayilan: T) {
const [deger, setDeger] = useState<T>(() => { // tembel başlangıç (2. ders):
try { // okuma yalnız İLK render'da
const ham = localStorage.getItem(anahtar);
return ham === null ? varsayilan : (JSON.parse(ham) as T);
} catch {
return varsayilan; // erişim engeli, bozuk JSON → hook yine çalışır
}
});
useEffect(() => {
try {
localStorage.setItem(anahtar, JSON.stringify(deger));
} catch {
// kota dolu ya da erişim yok: yazamadık ama uygulama kırılmaz
}
}, [anahtar, deger]);
return [deger, setDeger] as const;
}
// iki bağımsız kullanım — state paylaşılmaz, MANTIK paylaşılır:
const [taslak, setTaslak] = useLocalStorage("basvuru:taslak", "");
const [gorunum, setGorunum] = useLocalStorage<"liste" | "kart">("pano:gorunum", "liste");
Satır aralarındaki kararlar örnek setinin asıl dersi: tembel başlangıç
pahalı okumayı ilk render'a sabitler; try/catch iki uçta da var çünkü
localStorage fırlatabilir (tarayıcı site verisini engellemiş olabilir,
JSON bozuk gelebilir, kota dolabilir) ve bir hook'un uygulamayı bu yüzden düşürmesi
kabul edilemez; anahtar bağımlılıkta — çağıran anahtarı
değiştirirse yazma doğru yere gider. Dönüşün useState imzasını
kopyalaması bilinçli: çağıran, alışkın olduğu sözleşmeyi (fonksiyonel güncelleme
dahil, bedava) aynen kullanır. Eksik bıraktığımız bilinen sınır: iki sekme arası
senkron (storage olayı) — onu alıştırmada sen ekleyeceksin.
06useOnlineStatus: doğru ilkel useSyncExternalStore
"Kullanıcı çevrim dışı mı?" bilgisini tarayıcı verir: navigator.onLine
+ online/offline olayları. İlk refleks state + effect'tir
ve çalışır; ama React'in bu iş sınıfı için özel bir ilkeli vardır ve adı
mülakatta ayırt eder: useSyncExternalStore — "React dışında yaşayan bir
veri kaynağına abone ol, anlık görüntüsünü state gibi sun".
function abone(bildir: () => void) { // kaynak değişince React'e haber ver
window.addEventListener("online", bildir);
window.addEventListener("offline", bildir);
return () => { // aynı kur-yık sözleşmesi (3. ders)
window.removeEventListener("online", bildir);
window.removeEventListener("offline", bildir);
};
}
function useOnlineStatus(): boolean {
return useSyncExternalStore(
abone, // modül seviyesinde tanımlı → referansı zaten sabit
() => navigator.onLine // anlık görüntü: SENKRON okunur, yan etkisiz
);
}
State + effect sürümüne göre iki üstünlüğü var: ilk render doğru değerle
gelir (effect sürümü önce varsayılanı basar, effect çalışınca düzeltir — bir
karelik yanlış ekran) ve eşzamanlı render'da yırtılma (tearing) yaşamaz —
React render'ı böldüğünde iki bileşenin aynı kaynağı farklı görmesini bu ilkel
engeller. Karar çizgisi: veri React dışında yaşıyor ve kendi bildirim
mekanizması varsa (tarayıcı olayı, WebSocket istemcisi, üçüncü parti store)
useSyncExternalStore; veri React state'i olarak doğuyorsa bildiğin
useState. 9. derste göreceğin store kütüphaneleri React köprüsünü tam
da bu ilkelle kurar.
07İki disiplin: erken soyutlamama ve test edilebilirlik
Erken soyutlama uyarısı. Hook çıkarmak ucuzlaştıkça refleksleşir ve 4.
hattın 2. dersindeki ölçü tam burada devreye girer: üçüncü tekrara kadar bekle.
İki kullanım yeri, ortak soyutlamanın doğru şeklini göstermeye çoğu zaman yetmez;
erken çıkarılan hook, ikinci çağıranın ihtiyacıyla opsiyon parametreleri biriktirir
ve "yanlış soyutlama, tekrardan pahalıdır" cümlesine dönüşür. Sağlıklı sıra: önce
bileşen içinde yaz, tekrarı gör, örgü üçüncü kez aynı şekilde belirince
çıkar. Tek kullanımlık hook'un meşru istisnası okunabilirliktir: 400 satırlık
bileşenden useBasvuruFiltresi diye anlamlı bir parça koparmak,
paylaşım değil adlandırma kazancıdır ve geçerlidir.
Test edilebilirlik. Hook bileşensiz çağrılamaz — hook kuralları render bağlamı ister. Bundan iki pratik çıkar:
- Saf mantığı hook'tan dışarı al.
useLocalStorage'daki JSON okuma/yazma,useGecikmisDeger'deki süre kuralı gibi parçalar React'e muhtaç değilse ayrı saf fonksiyon olur ve doğrudan test edilir — 6. dersteki "reducer bileşensiz test edilir" ucuzluğunun aynısı. Hook inceldikçe test yükü düşer. - Hook'un kendisi için araç hazır: Testing Library'nin
renderHook'u, hook'u görünmez bir test bileşeni içinde çalıştırır ve dönüşünü sorgulatır; sahte zamanlayıcılarlauseGecikmisDeger'in 300 ms kuralı milisaniyesiyle doğrulanır. Mekanizmanın tamamı 15. hattın frontend test dersinde — buradaki görev, hook'u o güne ince ve saf çekirdekli teslim etmek.
08Mini alıştırma — Kendi hook rafın
Yaklaşık 20 dakika · kurulum yok — StackBlitz projenin üstüne kur
useLocalStorage'ı bu dersteki gibi yaz; başvuru formunun "not" alanını ona bağla. Sayfayı yenile, notun durduğunu gör; sonra DevTools → Application'dan değeri elle bozuk JSON yap ve hook'un varsayılana düşüp çökmediğini doğrula.- Aynı hook'u ikinci bir anahtar ile (
pano:gorunum) ikinci kez çağır; iki state'in bağımsız olduğunu kanıtla — "custom hook state paylaşmaz" cümlesini kendi kodunla söyle. - Eksik bırakılan sınırı kapat:
storageolayını dinleyen bir effect ekleyip iki sekme arasında senkron sağla. İki sekme açıp birinde notu değiştir, diğerinde güncellendiğini gör (olay yalnız diğer sekmelere gider — bunu da not et). useOnlineStatus'uuseSyncExternalStoreile kur ve üstbara "çevrim dışı" rozeti bağla; DevTools → Network → Offline ile test et. Sonra aynı hook'u state + effect ile yazıp ilk render karesindeki farkı açıkla.- Dönüş biçimi kararını yaşa:
useGecikmisDeger'i önceas const'suz dizi döndürecek şekilde boz, çağıran taraftaki tip bulanıklığını gözle;as constile düzelt. - Erken soyutlama muhakemesi: projendeki iki benzer effect'i bul (yoksa kur) ve hook'a çıkarmak için "üçüncü tekrar" ölçüsünü uygula — çıkar ya da bekle; kararını bir cümlelik gerekçeyle yaz.
09Kendini sına
Custom hook'un çağrı sözleşmesini anlat: parametreleri "reaktif girdi" yapan nedir ve iki bileşenin aynı hook'u kullanması neden state paylaşımı değildir?
Custom hook, bileşenin her render'ında yeniden çağrılan bir
fonksiyondur — "her render yeni çağrı" modeli hook'un içine de uzanır. Bu yüzden
parametreler tek seferlik yapılandırma değil, her render'da tazelenen canlı
değerlerdir; hook'un içindeki effect'ler bu değerleri bağımlılık olarak beyan
eder ve değişime tepki verir. Değişebilecek her şey parametre olarak tasarlanır,
hook içine sabit gömülmez. İkinci sonuç: her çağrı, çağıran bileşenin hücre
defterine kendi satırlarını ekler — iki bileşen aynı hook'u kullandığında iki
bağımsız state seti oluşur. Hook'un paylaştığı şey mantıktır (örgünün
kendisi), state değil; bileşenler arası paylaşılan state isteniyorsa yol, state'i
yukarı taşımak ya da Context/store'dur. use öneki de sözleşmenin
parçasıdır: lint, koşulsuz-sıralı çağrı kurallarını ancak bu önekle
denetleyebilir.
Hook'un dönüş tasarımını savun: hangi durumda dizi, hangi durumda nesne dönersin ve dışa verdiğin fonksiyonları neden useCallback'e alırsın?
İki elemanlı değer + eylem ikilisi dizi (tuple) döner —
useState deseninin kendisi: çağıran elemanları istediği adla alır ve
aynı hook'u iki kez farklı adlarla kullanabilir; TypeScript'te dönüşe
as const eklenir, yoksa çıkarım tuple değil bulanık dizi olur. Üç ve
üzeri alan ya da seçmeli kullanım varsa nesne döner: sıra ezberi yoktur, alanlar
seçilerek alınır, yeni alan eklemek mevcut çağıranları kırmaz. Referans
kararlılığı ise hook yazarının sorumluluğudur: çağıran, dönen fonksiyonu bir
effect bağımlılığına ya da memo'lu çocuğa geçirebilir; her render'da yeni referans
dönersen çağıranın effect'i her render'da tetiklenir. Bu, 6. dersteki
useCallback'in üçüncü meşru bağlamıdır — hook, çağıranının ne
yapacağını bilemeyeceği için kütüphane yazarı gibi savunmacı davranır: dışa
verilen fonksiyonlar useCallback, derlenmiş nesneler
useMemo ile sabitlenir.
useSyncExternalStore ne zaman doğru ilkeldir? useOnlineStatus örneği üzerinden state + effect sürümüne iki üstünlüğünü anlat.
Veri React'in dışında yaşıyorsa ve kendi bildirim
mekanizması varsa (tarayıcı olayı, WebSocket istemcisi, harici store)
useSyncExternalStore doğru ilkeldir: bir abone
fonksiyonu (kaynak değişince React'e haber verir, temizlik fonksiyonu döndürür)
ve bir anlık görüntü fonksiyonu (kaynağı senkron, yan etkisiz okur)
alır. useOnlineStatus'ta abonelik online/offline
olaylarına, görüntü navigator.onLine'a bakar. State + effect
sürümüne iki üstünlüğü: (1) ilk render doğru değerle gelir — effect
sürümü önce varsayılanı basıp effect çalışınca düzeltir, bir karelik yanlış ekran
üretir; (2) eşzamanlı render'da yırtılmayı (tearing) engeller — React
render'ı böldüğünde iki bileşenin aynı kaynağı farklı anlık değerlerle görmesi bu
ilkelle imkânsızlaşır. Veri React state'i olarak doğuyorsa gereksizdir —
useState kalır. Store kütüphanelerinin React köprüsü de bu ilkelle
kurulur.
"Her tekrarı hemen hook'a çıkar" diyen ekip arkadaşına ne dersin? Erken soyutlama ölçüsünü ve hook'ların test stratejisini birlikte anlat.
Ölçü, 4. hattaki üç tekrar kuralıdır: ikinci kullanım yeri,
ortak soyutlamanın doğru şeklini göstermeye çoğu zaman yetmez; erken çıkarılan
hook, sonraki çağıranların ihtiyaçlarıyla opsiyon parametresi biriktirir ve
"yanlış soyutlama tekrardan pahalıdır" durumuna düşer. Doğru sıra: bileşen içinde
yaz, tekrarı gör, örgü üçüncü kez aynı biçimde belirince çıkar. İstisna
adlandırma kazancıdır: dev bir bileşenden anlamlı bir parçayı tek kullanımlık
hook'a koparmak paylaşım değil okunabilirlik işidir ve meşrudur. Test stratejisi
iki katmanlıdır: React'e muhtaç olmayan saf mantık (JSON çözme, süre kuralı,
dönüşüm) hook'tan ayrı saf fonksiyonlara alınır ve doğrudan, ucuzca test edilir —
reducer testinin ucuzluğuyla aynı ilke; hook'un kendisi ise render bağlamı
istediği için Testing Library'nin renderHook'u ile görünmez bir test
bileşeni içinde çalıştırılır (sahte zamanlayıcılarla süre kuralları da
doğrulanır). Hook ne kadar ince, çekirdek ne kadar safsa test o kadar ucuz —
ayrıntı 15. hatta.
10Sınav köşesi
Yazılıda nasıl sorulur
(1) Custom hook nedir; sıradan fonksiyondan farkı nedir? (2) use
öneki neden zorunludur? (3) İki bileşen aynı custom hook'u kullanırsa state
paylaşılır mı? (4) Hook dönüşünde dizi ve nesne biçimi hangi durumda seçilir?
(5) as const hook dönüşünde neyi çözer? (6) Hook'un dışa verdiği
fonksiyonlar neden referans kararlı olmalıdır? (7) useSyncExternalStore
hangi iş sınıfı içindir; iki parametresi nedir? (8) Debounce ile throttle farkı
nedir? (9) Erken soyutlamanın maliyeti nedir; ölçüsü nedir? (10) Hook'lar nasıl
test edilir; saf mantığı ayırmanın faydası nedir?
Sözlüde nasıl anlatılır — 2 dakikalık taslak
Düzen: (1) Tanım — "Custom hook, içinde hook çağıran ve her render'da yeniden çalışan fonksiyondur; parametreleri canlı reaktif girdilerdir ve her çağrı bağımsız state kurar — state değil mantık paylaşır." (2) API tasarımı — "İkili dönüşte tuple + as const, üç ve üzeri alanda nesne dönerim; dışa verdiğim fonksiyonları useCallback'le sabitlerim çünkü çağıranın effect bağımlılığına girebilirler — hook yazarı kütüphaneci gibi savunmacıdır." (3) Örnek raf — "useLocalStorage'da tembel başlangıç ve iki uçta try/catch; useOnlineStatus'ta doğru ilkel useSyncExternalStore: ilk render doğru gelir, eşzamanlı render'da yırtılma olmaz." (4) Disiplin — "Üçüncü tekrara kadar hook çıkarmam — yanlış soyutlama tekrardan pahalıdır; tek istisna adlandırma kazancı." (5) Kıdem vuruşu — "Saf mantığı hook'tan ayırırım: çekirdek fonksiyonlar bileşensiz test edilir, hook'un kendisi renderHook'la — ince hook, ucuz test demektir."
