19.12.2025

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 (
    <div>
      <input
        value={q}
        onChange={(e) => setQ(e.target.value)}
        placeholder="Ürün ara…"
      />

      {loading && <p>Yükleniyor…</p>}
      {error && <p>Hata: {String(error)}</p>}

      <ul>
        {data.map((p) => (
          <li key={p.id}>{p.name}</li>
        ))}
      </ul>
    </div>
  );
}

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

  1. Tek sorumluluk: useDebouncedValue sadece debouncing yapıyor; URL işiyle karışmıyor.
  2. Bağımlılıkları dışarıdan al: fetcher fonksiyonunu parametre geçmek, test etmeyi kolaylaştırır.
  3. İptal/cleanup unutma: AbortController veya abonelik temizliği, “setState on unmounted” problemlerini azaltır.
  4. 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.