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.