React’te Custom Hook Tasarımı: UI’dan Bağımsız Mantık Katmanı Kurmak
Custom hook’larla tekrar eden iş mantığını UI’dan ayırın; test edilebilir, okunabilir React kodu yazın.
Neden custom hook?
React projeleri büyüdükçe asıl tekrar eden şey çoğu zaman UI değil, iş mantığıdır: form doğrulama, sorgu parametresi yönetimi, debounced arama, localStorage senkronu, websocket abonelikleri…
Custom hook yaklaşımıyla bu mantığı bileşenlerden ayırıp:
- Aynı davranışı farklı UI’larda tekrar kullanır,
- Daha küçük bileşenler yazarsınız,
- Mantığı izole şekilde test etmek kolaylaşır.
Aşağıda “yeni bir açı”: UI’dan tamamen bağımsız, farklı ekranlarda çalışacak bir arama deneyimini küçük parçalar halinde kuralım.
Örnek 1: Debounced arama için useDebouncedValue
Kullanıcı yazarken her tuş vuruşunda istek atmak yerine değeri geciktirelim.
import { useEffect, useState } from "react";
export function useDebouncedValue(value, delay = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
Bu hook saf mantık: input, modal, header araması… nerede isterseniz kullanırsınız.
Örnek 2: URL ile senkron çalışan arama: useQueryParam
Arama teriminin URL’de durması (paylaşılabilir link, geri/ileri tuşları) iyi bir kullanıcı deneyimidir.
React Router v6 kullanan bir senaryo:
import { useSearchParams } from "react-router-dom";
import { useCallback } from "react";
export function useQueryParam(key) {
const [params, setParams] = useSearchParams();
const value = params.get(key) ?? "";
const setValue = useCallback(
(next) => {
const copy = new URLSearchParams(params);
if (!next) copy.delete(key);
else copy.set(key, String(next));
setParams(copy, { replace: true });
},
[key, params, setParams]
);
return [value, setValue];
}
Hook’ı kullanan bileşen URL detayını bilmek zorunda kalmaz.
Örnek 3: Hepsini birleştiren “arama modeli” hook’u
Şimdi UI’dan bağımsız bir “arama modeli” oluşturalım: input değeri URL ile senkron olsun, istekler debounced gitsin, iptal edilebilir olsun.
import { useEffect, useMemo, useState } from "react";
import { useDebouncedValue } from "./useDebouncedValue";
import { useQueryParam } from "./useQueryParam";
export function useProductSearch(fetcher) {
const [q, setQ] = useQueryParam("q");
const debouncedQ = useDebouncedValue(q, 400);
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const ac = new AbortController();
async function run() {
setLoading(true);
setError(null);
try {
const result = await fetcher(debouncedQ, { signal: ac.signal });
setData(result);
} catch (e) {
if (e.name !== "AbortError") setError(e);
} finally {
setLoading(false);
}
}
run();
return () => ac.abort();
}, [debouncedQ, fetcher]);
return useMemo(
() => ({ q, setQ, debouncedQ, data, loading, error }),
[q, setQ, debouncedQ, data, loading, error]
);
}
UI katmanı (örnek kullanım)
function ProductSearchPage({ api }) {
const { q, setQ, data, loading, error } = useProductSearch(api.searchProducts);
return (
setQ(e.target.value)}
placeholder="Ürün ara…"
/>
{loading && Yükleniyor…
}
{error && Hata: {String(error)}
}
{data.map((p) => (
{p.name}
))}
);
}
Burada önemli nokta: Sayfa sadece render eder. Arama akışının tüm karmaşıklığı hook içinde kalır.
İyi custom hook tasarımı için 4 kısa kural
- Tek sorumluluk:
useDebouncedValuesadece debouncing yapıyor; URL işiyle karışmıyor. - Bağımlılıkları dışarıdan al:
fetcherfonksiyonunu parametre geçmek, test etmeyi kolaylaştırır. - İptal/cleanup unutma:
AbortControllerveya abonelik temizliği, “setState on unmounted” problemlerini azaltır. - UI’yı zorlamama: Hook HTML bilmemeli; sadece veri/aksiyon döndürmeli.
Sonuç
Custom hook’lar, React’te “bileşen tekrar kullanımı”ndan farklı olarak mantık tekrarını hedefler. Debounce + URL senkronu + iptal edilebilir istek gibi parçaları bir araya getirerek daha temiz bir mimari kurabilirsiniz.