31.12.2025

React’te URL’yi Uygulamanın Gerçek Kaynağı Yapmak: Search Params ile Filtre, Sıralama ve Paylaşılabilir Ekranlar

Filtre/sıralama state’ini URL’ye taşıyarak paylaşılabilir, geri-ileri uyumlu ve kalıcı React ekranları tasarlayın.

React uygulamalarında liste ekranı yaparken filtreler, arama kutusu ve sıralama seçenekleri genelde useState ile tutulur. Sonuç: sayfayı yenileyince her şey sıfırlanır, link paylaşınca aynı görünüm yakalanamaz, geri/ileri (back/forward) davranışı tutarsızlaşır.

Bu yazıda farklı bir açıdan bakıyoruz: UI state’in bir kısmını URL’nin kendisine taşıyıp “kaynağı URL olan” ekranlar tasarlamak.

Ne zaman URL’de tutmalı?

Aşağıdaki bilgiler genelde URL için uygundur:

  • Arama metni (q)
  • Filtreler (status=active, tag=react)
  • Sıralama (sort=createdAt, order=desc)
  • Sayfalama (page=2)

Aşağıdakiler ise çoğunlukla URL’ye yazılmaz:

  • Modal açık/kapalı (bazen yazılabilir)
  • Input’un “odak” durumu
  • Geçici UI animasyon state’i

Örnek: Ürün listesi filtreleri (Search Params)

React Router v6.4+ ile useSearchParams kullanarak başlayalım.

1) Parametreleri tek bir yerden okuyup yazmak

import { useMemo, useCallback } from "react";
import { useSearchParams } from "react-router-dom";

function parseIntSafe(value, fallback) {
  const n = Number.parseInt(value ?? "", 10);
  return Number.isFinite(n) && n > 0 ? n : fallback;
}

export function useProductQuery() {
  const [params, setParams] = useSearchParams();

  const query = useMemo(() => {
    const q = params.get("q") ?? "";
    const status = params.get("status") ?? "all"; // all | active | passive
    const sort = params.get("sort") ?? "createdAt"; // createdAt | price
    const order = params.get("order") ?? "desc"; // asc | desc
    const page = parseIntSafe(params.get("page"), 1);

    return { q, status, sort, order, page };
  }, [params]);

  const update = useCallback(
    (patch) => {
      setParams((prev) => {
        const next = new URLSearchParams(prev);

        for (const [key, value] of Object.entries(patch)) {
          // Boş değerleri URL'den temizlemek iyi bir pratiktir
          if (value === undefined || value === null || value === "") {
            next.delete(key);
          } else {
            next.set(key, String(value));
          }
        }

        // Filtre değişince sayfayı 1'e çekmek genelde beklenen davranış
        if ("q" in patch || "status" in patch || "sort" in patch || "order" in patch) {
          next.set("page", "1");
        }

        return next;
      });
    },
    [setParams]
  );

  return { query, update };
}

2) UI bileşenlerini URL ile senkron yapmak

import { useEffect, useState } from "react";
import { useProductQuery } from "./useProductQuery";

export default function ProductListPage() {
  const { query, update } = useProductQuery();

  // Arama input’u için küçük bir debounce (URL'yi her tuşta spam’lemeyelim)
  const [draft, setDraft] = useState(query.q);
  useEffect(() => setDraft(query.q), [query.q]);

  useEffect(() => {
    const t = setTimeout(() => update({ q: draft }), 300);
    return () => clearTimeout(t);
  }, [draft, update]);

  return (
    <div>
      <h1>Ürünler</h1>

      <input
        value={draft}
        placeholder="Ara (örn. kulaklık)"
        onChange={(e) => setDraft(e.target.value)}
      />

      <select value={query.status} onChange={(e) => update({ status: e.target.value })}>
        <option value="all">Hepsi</option>
        <option value="active">Aktif</option>
        <option value="passive">Pasif</option>
      </select>

      <select value={query.sort} onChange={(e) => update({ sort: e.target.value })}>
        <option value="createdAt">Tarih</option>
        <option value="price">Fiyat</option>
      </select>

      <button onClick={() => update({ order: query.order === "asc" ? "desc" : "asc" })}>
        Sıra: {query.order}
      </button>

      <hr />

      <Pagination page={query.page} onPageChange={(p) => update({ page: p })} />

      <ProductsTable query={query} />
    </div>
  );
}

function Pagination({ page, onPageChange }) {
  return (
    <div style={{ display: "flex", gap: 8 }}>
      <button disabled={page <= 1} onClick={() => onPageChange(page - 1)}>
        Önceki
      </button>
      <span>Sayfa: {page}</span>
      <button onClick={() => onPageChange(page + 1)}>Sonraki</button>
    </div>
  );
}

function ProductsTable({ query }) {
  // Burada query'yi API isteğine map edebilirsiniz.
  // Örn: GET /api/products?q=...&status=...&sort=...&order=...&page=...
  return <pre>{JSON.stringify(query, null, 2)}</pre>;
}

Bu yaklaşımın getirileri

  • Paylaşılabilir ekran: Aynı URL = aynı görünüm.
  • Yenilemede kalıcılık: F5 sonrası filtreler korunur.
  • Back/forward uyumu: Kullanıcı tarayıcı geçmişiyle doğal gezinir.
  • Debug kolaylığı: “Hangi filtre açıktı?” sorusunun cevabı URL’de.

Dikkat edilmesi gerekenler

  • URL parametrelerini “temiz” tutun: boş değerleri silin.
  • Debounce kullanın: arama input’u gibi alanlar URL’yi gereksiz şişirmesin.
  • Tip güvenliği istiyorsanız, parametre parse/serialize fonksiyonlarını tek yerde toplayın.

Kapanış

React’te her state’i bileşen içinde tutmak zorunda değilsiniz. Liste/filtre gibi “ekranı tanımlayan” state’i URL’ye taşıdığınızda hem kullanıcı deneyimi hem de bakım maliyeti belirgin şekilde iyileşir. Bir sonraki liste ekranınızda “kaynak kim?” sorusunun cevabını URL yapmayı deneyin.