IlmHamroh
JavaScript Full-stack/19-qism. React: routing, server state, formalar9/36-dars19 daqiqa
Mundarija (26)

URL holat sifatida: filtr, saralash va sahifalash — useSearchParams, debounce, nuqs

Qisqacha: Ulashsa bo'ladigan holat — filtr, saralash, qidiruv, sahifa raqami — URL'ning so'rov qismida (?filtr=faol&sahifa=2) turadi. Qoidalar: qiymat URL'dan oq ro'yxat bilan o'qiladi (begona qiymat — standart); standart qiymat URL'ga yozilmaydi; o'zgarmagan qiymat navigatsiya qilmaydi; tugma bosish — push (tarixda yozuv), har harfda yoziladigan qidiruv — replace va debounce. Tarixning egasi bitta bo'lsin: router bor joyda o'z history.pushState ingizni yozmang. Kutubxona — nuqs (useQueryState va parser'lar).

Bu darsda

  • Qaysi holat URL'ga, qaysi biri useState ga borishini tanlaysiz.
  • URL'dan qiymatni oq ro'yxat bilan o'qiysiz va standart qiymatni URL'dan tozalaysiz.
  • Qidiruvni debounce bilan, replace orqali URL'ga yozasiz.
  • Sahifalashni URL'da qurasiz: ?sahifa= ni tekshirasiz va chegaralaysiz.
  • nuqs kutubxonasi nima berishini bilasiz va vazifalar filtrini o'z pushState dan React Router'ga ko'chirasiz.

Oldin bilishingiz kerak: React Router III: URL holati, URL va URLSearchParams, useSyncExternalStore.

1. Nega bu kerak?

04-darsda useSearchParams bilan tanishdik va qoida chiqardik: "havola bilan birga ketishi kerak bo'lgan holat — URL'da". Bugun shu qoidani chuqurlashtiramiz, chunki "URL'ga yozish" ning o'z tuzoqlari bor.

«Bahor» menyusida uchta tanlov bor: taom turi, narx bo'yicha saralash, sahifa. Jasur aka uchta shikoyat aytdi:

  1. "Do'stimga «ichimliklar, arzonidan, 2-sahifa» havolasini yubordim — u boshqa narsani ko'rdi." Bir tanlov URL'da, ikkitasi useState da edi.
  2. "Qidiruvga «choy» yozdim, keyin «Orqaga» ni to'rt marta bosdim." Har harf — tarixda alohida yozuv.
  3. "Havolada ?sahifa=999 — sahifa bo'sh, xato ham yo'q." URL'dagi qiymat tekshirilmagan.

vazifalar da yana bittasi bor, u ko'zga ko'rinmaydi. 17-qismdan beri filtr URL'ga o'zimiz yozgan history.pushState bilan tushadi. Router esa tarixni o'zi sanaydi. Ikki ega bitta tarixni boshqarsa nima bo'lishini kanon o'lchadi — «Vazifalar qadami» da.

Hayotiy o'xshatish: URL — buyurtma varaqasi. Ofitsiantning boshidagi eslatma (useState) boshqa ofitsiantga o'tmaydi. Varaqaga yozilgan narsa esa kimning qo'liga tushsa ham bir xil tayyorlanadi. Lekin varaqani mehmon ham to'ldirishi mumkin — oshpaz "pitsa" deb yozilganini tekshirmasdan pishirmaydi.

2. URL'dan o'qish: oq ro'yxat va standart

2.1 Qaysi holat qayerda

Holat Joyi Nega
filtr, saralash, qidiruv, sahifa URL ulashiladi, yangilanishda va «Orqaga» da qoladi
ochiq tab, tanlangan bo'lim URL (ko'pincha) havola aniq joyga olib borsin
forma qoralamasi, input matni useState yozish jarayoni — ulashilmaydi
ochiq menyu, hover, modal useState vaqtinchalik UI
serverdan kelgan ro'yxat server kesh Server state vs client state

Chegaraviy holatlar ham bor. Qidiruv maydonidagi matn — ham useState (har harf), ham URL (debounce bilan). Modal ba'zan URL'da: "rasmni kattalashtirib ko'rsatish" havolasini ulashish uchun (/taom/osh?rasm=2).

2.2 Oq ro'yxat bilan o'qish

URL — foydalanuvchi kiritgan ma'lumot. searchParams.get("turi") ning qiymati "issiq" ham, "pitsa" ham, "<script>" ham bo'lishi mumkin. Shuning uchun o'qish oq ro'yxat (allowlist) bilan: ruxsat etilgan qiymatlardan biri bo'lsa — o'zi, bo'lmasa — standart.

jsx
import {
  createMemoryRouter,
  RouterProvider,
  useLocation,
  useSearchParams,
} from "react-router";

const TYPES = ["hammasi", "issiq", "ichimlik"];
const MENU = [
  { name: "Osh", type: "issiq" },
  { name: "Ko'k choy", type: "ichimlik" },
];

// Oq ro'yxat: yo'q yoki begona — standart
function readParam(params, name, allowed, fallback) {
  const value = params.get(name);
  return allowed.find((a) => a === value) ?? fallback;
}

// Standart qiymat URL'ga yozilmaydi
function setParam(params, name, value, fallback) {
  const next = new URLSearchParams(params);
  if (value === fallback) next.delete(name);
  else next.set(name, value);
  return next;
}

function Menu() {
  const { search } = useLocation();
  const [params, setParams] = useSearchParams();
  const type = readParam(params, "turi", TYPES, "hammasi");
  const shown = MENU.filter(
    (d) => type === "hammasi" || d.type === type,
  );
  const choose = (value) =>
    setParams(setParam(params, "turi", value, "hammasi"));
  return (
    <section>
      <p>URL: {search || "(bo'sh)"}</p>
      <button onClick={() => choose("hammasi")}>Hammasi</button>{" "}
      <button onClick={() => choose("ichimlik")}>Ichimliklar</button>
      <ul>
        {shown.map((d) => (
          <li key={d.name}>{d.name}</li>
        ))}
      </ul>
    </section>
  );
}

const router = createMemoryRouter(
  [{ path: "/menyu", element: <Menu /> }],
  { initialEntries: ["/menyu?turi=pitsa&til=uz"] },
);

export default function App() {
  return <RouterProvider router={router} />;
}

Ekranda:

text
URL: ?til=uz
Hammasi Ichimliklar
Osh
Ko'k choy

Uch narsani kuzating:

  1. Ilova ?turi=pitsa bilan ochildi — yiqilmadi, bo'sh ro'yxat ham bermadi: readParam "pitsa" ni tanimadi va "hammasi" ni qaytardi.
  2. «Hammasi» bosilganda turi URL'dan o'chdi. ?turi=hammasi va ? — bir xil sahifa. Bitta sahifa — bitta manzil: havola qisqa, Google ham bitta sahifa deb biladi.
  3. til=uz — biz tegmagan parametr — joyida qoldi. setParam eski parametrlardan nusxa olib, faqat bittasini o'zgartiradi (04-darsdagi "funksiya bilan yozish" ning sof varianti).

readParam va setParam — sof funksiyalar: router'siz, brauzersiz node:test da tekshiriladi. vazifalar dagi parametrniOl va parametrniQoy aynan shunday.

Tekshirib ko'ring: Ilova ?turi=ichimlik bilan ochildi va foydalanuvchi «Ichimliklar» ni yana bosdi. Tarixga yangi yozuv qo'shiladimi?

Javob

Bu misolda — ha: setParams URL o'zgarmasa ham navigatsiya qiladi va tarixga yozuv qo'shadi. «Orqaga» bosilganda hech narsa o'zgarmaydi — foydalanuvchi "tugma ishlamayapti" deb o'ylaydi. Davosi — yozishdan oldin solishtirish: if (next.toString() !== params.toString()). vazifalar dagi useUrlTanlovi shunday qiladi — «Vazifalar qadami» da.

3. Yozish: push, replace va debounce

3.1 Qaysi o'zgarish — tarixda yozuv?

O'zgarish Usul Nega
filtr, saralash, sahifa tugmasi push (sukut) har bosish — foydalanuvchi qadami, «Orqaga» uni qaytarsin
qidiruvga yozish replace har harf — qadam emas
noto'g'ri qiymatni tuzatish (?sahifa=999 → ?sahifa=5) replace yo'naltirish — 04-dars qoidasi

3.2 Debounce: yozish tugaganda

replace tarixni saqlaydi, lekin har harfda baribir navigatsiya bo'ladi: router hamma useSearchParams ni qayta chizadi, loader bo'lsa — serverga so'rov. "choy" — to'rt so'rov. Debounce — "foydalanuvchi yozishdan to'xtasin, keyin bir marta". Input useState da (har harf darhol ko'rinadi), URL'ga esa 300 ms jimlikdan keyin yoziladi:

jsx
import { useEffect, useState } from "react";
import {
  createMemoryRouter,
  RouterProvider,
  useLocation,
  useSearchParams,
} from "react-router";

function Search() {
  const { search } = useLocation();
  const [params, setParams] = useSearchParams();
  // Input — darhol (useState), URL — keyinroq
  const [text, setText] = useState(params.get("q") ?? "");

  useEffect(() => {
    const timer = setTimeout(() => {
      const next = new URLSearchParams(params);
      if (text === "") next.delete("q");
      else next.set("q", text);
      if (next.toString() !== params.toString()) {
        console.log("URL'ga yozildi:", text);
        setParams(next, { replace: true });
      }
    }, 300);
    // yangi harf — eski taymer bekor
    return () => clearTimeout(timer);
  }, [text, params, setParams]);

  return (
    <section>
      <input
        aria-label="Qidiruv"
        value={text}
        onChange={(e) => setText(e.target.value)}
      />
      <p>URL: {search || "(bo'sh)"}</p>
    </section>
  );
}

const router = createMemoryRouter(
  [{ path: "/menyu", element: <Search /> }],
  { initialEntries: ["/menyu"] },
);

export default function App() {
  return <RouterProvider router={router} />;
}

Konsolda:

text
URL'ga yozildi: choy

Ekranda:

text
URL: ?q=choy

Yozilgan zahoti URL hali bo'sh edi (ekran qadami tekshirdi). 300 ms dan keyin — bitta yozuv, replace bilan. Har yangi harf oldingi taymerni bekor qiladi (effekt tozalash) — tez yozilgan "choy" bitta navigatsiya bo'ladi.

Maslahat: Qachon debounce kerak emas? Tugma va select — bitta bosish, bitta qiymat. Debounce faqat "ketma-ket ko'p o'zgarish" uchun: matn, surgich (input type="range"). 300 ms — odatiy tanlov: odam yozayotganda harflar orasidagi pauza undan qisqa.

4. Sahifalash URL'da

4.1 ?sahifa= — son, chegara ichida

Sahifa raqami — URL holatining eng ko'p uchraydigan turi. U son, shuning uchun tekshiruv uch qadamli: songa aylantirish, butun sonmi, chegara ichidami. 999-sahifa so'ralsa, eng oxirgisini ko'rsatish mantiqiy:

jsx
import {
  createMemoryRouter,
  Link,
  RouterProvider,
  useLocation,
  useSearchParams,
} from "react-router";

const DISHES = ["Osh", "Manti", "Lag'mon", "Somsa", "Chuchvara"];
const SIZE = 2;

function readPage(params, last) {
  const n = Number(params.get("sahifa") ?? "1");
  if (!Number.isInteger(n) || n < 1) return 1;
  return Math.min(n, last);
}

function pageHref(params, page) {
  const next = new URLSearchParams(params);
  if (page === 1) next.delete("sahifa");
  else next.set("sahifa", String(page));
  const s = next.toString();
  return s === "" ? "?" : `?${s}`;
}

function Menu() {
  const { search } = useLocation();
  const [params] = useSearchParams();
  const last = Math.ceil(DISHES.length / SIZE);
  const page = readPage(params, last);
  const items = DISHES.slice((page - 1) * SIZE, page * SIZE);
  return (
    <section>
      <p>URL: {search || "(bo'sh)"}</p>
      <ul>
        {items.map((d) => (
          <li key={d}>{d}</li>
        ))}
      </ul>
      <nav aria-label="Sahifalar">
        {page > 1 && (
          <Link to={pageHref(params, page - 1)}>‹ Oldingi</Link>
        )}{" "}
        {page}-sahifa / {last}{" "}
        {page < last && (
          <Link to={pageHref(params, page + 1)}>Keyingi ›</Link>
        )}
      </nav>
    </section>
  );
}

const router = createMemoryRouter(
  [{ path: "/menyu", element: <Menu /> }],
  { initialEntries: ["/menyu?sahifa=999&turi=issiq"] },
);

export default function App() {
  return <RouterProvider router={router} />;
}

Ekranda:

text
URL: ?sahifa=2&turi=issiq
Lag'mon
Somsa
‹ Oldingi 2-sahifa / 3 Keyingi ›

?sahifa=999 — oxirgi, 3-sahifa (yiqilmadi, bo'sh ham emas). «‹ Oldingi» — ?sahifa=2, turi=issiq saqlandi. Bir necha detal:

  • Sahifa tugmasi — <Link>, <button> emas. Har sahifa — haqiqiy manzil: yangi tabda ochish mumkin, Google keyingi sahifalarni topadi (Link yoki navigate jadvali).
  • 1-sahifa URL'da yo'q — standart qiymat tozalanadi.
  • <nav aria-label="Sahifalar"> — Malika (ekran o'quvchi) sahifalash blokini nomi bilan topadi.
  • Yaxshiroq variant — ?sahifa=999 ni ko'rganda replace bilan ?sahifa=3 ga yo'naltirish, toki manzil ham to'g'ri bo'lsin. Bu — 3-mashq.

Sahifadagi ma'lumot serverdan kelsa, sahifa loader'ga ham boradi: new URL(request.url).searchParams (data mode). Sahifalar orasida eski ma'lumotni ushlab turish va "yana yuklash" — TanStack Query IV darsida.

5. nuqs — URL holati uchun kutubxona

readParam, setParam, debounce, standart qiymat, tur — har loyihada qayta yoziladigan kod. nuqs uni bitta hook'ga yig'adi: useQueryState — useState ga o'xshaydi, lekin qiymat URL'da.

tsx
import {
  parseAsInteger,
  parseAsStringLiteral,
  useQueryState,
} from "nuqs";

const TURLAR = ["hammasi", "issiq", "ichimlik"] as const;

function Menyu() {
  // ?sahifa= — son; yo'q yoki yaroqsiz — 1; 1 bo'lsa URL'dan
  // o'chadi
  const [sahifa, setSahifa] = useQueryState(
    "sahifa",
    parseAsInteger.withDefault(1),
  );
  // ?turi= — faqat ro'yxatdagilar (oq ro'yxat)
  const [turi, setTuri] = useQueryState(
    "turi",
    parseAsStringLiteral(TURLAR).withDefault("hammasi"),
  );
  // ?q= — har harfda emas: replace + kechiktirish
  const [q, setQ] = useQueryState("q", {
    defaultValue: "",
    history: "replace",
  });
  // …
}

Bugungi darsning hamma qoidasi bir joyda:

Qoida nuqs da
oq ro'yxat, tur parser'lar: parseAsInteger, parseAsStringLiteral, parseAsBoolean…
standart qiymat tozalanadi .withDefault(…) — qiymat standartga teng bo'lsa URL'dan o'chadi
push yoki replace history: "push" / "replace"
bir nechta parametr bitta yozuvda useQueryStates({ … })
debounce yozishni kechiktirish sozlamasi bor (hujjatdan tekshiring)

nuqs router'ga adapter orqali ulanadi: ilova ildizini <NuqsAdapter> bilan o'raysiz. Next.js, TanStack Router va React Router'ning har versiyasi uchun alohida adapter bor. 2026-10-14 holatiga nuqs 2.10.2 (2026-10-08 da chiqqan) React Router 8 ni rasman qo'llaydi: peerDependencies da "react-router": "^5 || ^6 || ^7 || ^8", adapter — nuqs/adapters/react-router/v8 (npm view nuqs bilan tekshirildi).

Diqqat: nuqs kanonda o'lchanmagan va natija oynasida yo'q — yuqoridagi blok tekshirilmagan, to'plam hajmi ham o'lchanmagan. vazifalar da ikki parametr bor va ikkita sof funksiya yetarli — kutubxona qo'shilmadi.

6. Vazifalar qadami: filtr va saralash router'ga (holat 09)

6.1 Kanon o'lchovi: ikki ega — bir tarix

17-qismdan beri vazifalar filtri URL'ga o'zimizning history.pushState(null, "", url) bilan yoziladi (useSyncExternalStore darsi). 19/#02 dan esa sahifalarni React Router boshqaradi. Router har tarix yozuviga o'z ma'lumotini qo'yadi: { usr, key, idx } (idx — tarixdagi o'rni). Bizning pushState(null) yozuvlarida bu ma'lumot yo'q.

Kanon Chrome 155 da besh qadamni bajarib, har qadamda history.state ni yozib oldi (natijalar/09-tarix-oldin.txt, aynan):

text
ochildi                            /  history.state={"idx":0}
filtr «Faol» bosildi               /?filtr=faol  history.state=null
saralash «alifbo»                  /?filtr=faol&saralash=alifbo  history.state=null
«Batafsil» (router)                /vazifa/1  history.state={"usr":null,"key":"09dtgdck","idx":1}
Orqaga (popstate)                  /?filtr=faol&saralash=alifbo  history.state=null

To'rtinchi qatorga qarang: «Batafsil» — aslida tarixdagi to'rtinchi yozuv (idx: 3 bo'lishi kerak). Router esa idx: 1 deb hisobladi — filtr va saralash yozuvlarini u ko'rmagan. Beshinchi qator: «Orqaga» dan keyin state=null — router bu yozuvni tanimaydi, unda key yo'q.

flowchart LR
  A["/ <br/>idx 0"] --> B["?filtr=faol<br/>state: null"]
  B --> C["…&saralash=alifbo<br/>state: null"]
  C --> D["/vazifa/1<br/>router: idx 1"]
  D -. "aslida 4-yozuv" .-> X["idx 3 bo'lishi kerak"]

Hozircha ekranda xato ko'rinmaydi. Lekin 04-darsdagi «Orqaga» tugmasi key va navigatsiya turiga tayanadi; key siz yozuvlar — vaqtli bomba. Qoida: tarixning egasi bitta bo'lsin. Router bor joyda URL'ni faqat router orqali o'zgartiring.

bash
git switch -c refactor/url-router

6.2 marshrut.ts — faqat sof qism

O'z pushState imiz, obunachilar ro'yxati va popstate tinglovchisi o'chirildi. Faylda faqat o'qish va yangi parametrlar yasash qoldi (kanondan aynan, izoh qisqartirilgan):

ts
/** To'liq manzil (testlar) yoki router bergan parametrlar */
type Manba = string | URLSearchParams;

const parametrlar = (manba: Manba): URLSearchParams =>
  typeof manba === "string" ? new URL(manba).searchParams : manba;

// URL'dagi qiymat — oq ro'yxatdan; yo'q yoki begona bo'lsa —
// standart
function parametrniOl<T extends string>(
  manba: Manba,
  nomi: string,
  ruxsatlar: readonly T[],
  standart: T,
): T {
  const qiymat = parametrlar(manba).get(nomi);
  return ruxsatlar.find((ruxsat) => ruxsat === qiymat) ?? standart;
}

/**
 * Yangi parametrlar (eskisi o'zgarmaydi): standart qiymat URL'ga
 * yozilmaydi, boshqa parametrlar (muvaffaqiyatsiz=1...) joyida qoladi
 */
export function parametrniQoy(
  eski: URLSearchParams,
  nomi: string,
  qiymat: string,
  standart: string,
): URLSearchParams {
  const yangi = new URLSearchParams(eski);
  if (qiymat === standart) {
    yangi.delete(nomi);
  } else {
    yangi.set(nomi, qiymat);
  }
  return yangi;
}

«Oq ro'yxat bilan o'qish» bo'limidagi readParam/setParam ning TypeScript varianti. parametrniOl<T extends string> — generic: ruxsatlar Filtr[] bo'lsa, natija ham Filtr. find ning natijasi — T | undefined, ?? standart uni T qiladi.

6.3 useUrlTanlovi — router orqali

Hook endi useSyncExternalStore o'rniga useSearchParams ni ishlatadi (kanondan aynan):

ts
export function useUrlTanlovi() {
  const [parametrlar, setParametrlar] = useSearchParams();
  // Yozuv await'dan keyin ham bo'lishi mumkin (vazifa qo'shildi —
  // filtr "hammasi" ga): render paytidagi parametrlar eskirgan
  // bo'ladi. Eng so'nggisi — ref'da (commit'dan keyin yoziladi)
  const songgi = useRef(parametrlar);
  useLayoutEffect(() => {
    songgi.current = parametrlar;
  });

  // URL o'zgarmasa — navigatsiya (va tarix yozuvi) yo'q
  function yoz(nomi: string, qiymat: string, standart: string) {
    const eski = songgi.current;
    const yangi = parametrniQoy(eski, nomi, qiymat, standart);
    if (yangi.toString() !== eski.toString()) {
      songgi.current = yangi;
      // preventScrollReset: filtr — o'sha sahifa, aylantirilgan joy
      // qolsin
      void setParametrlar(yangi, { preventScrollReset: true });
    }
  }

  return {
    filtr: filtrniOl(parametrlar),
    saralash: saralashniOl(parametrlar),
    filtrniYoz: (filtr: Filtr) => yoz("filtr", filtr, "hammasi"),
    saralashniYoz: (saralash: Saralash) =>
      yoz("saralash", saralash, "standart"),
  };
}

Uch nozik joy:

  1. yangi.toString() !== eski.toString() — «Tekshirib ko'ring» dagi savolning javobi: o'sha filtr qayta bosilsa, tarix o'smaydi.
  2. songgi ref. Vazifa qo'shilgach, await dan keyin kod filtrni "hammasi" ga qaytaradi. Shu paytgacha foydalanuvchi saralashni o'zgartirgan bo'lishi mumkin. Render paytidagi parametrlar — eski nusxa (state — snapshot). Ref har commit'dan keyin yangilanadi va yozuv eng so'nggi parametrlardan boshlanadi.
  3. preventScrollReset: true — filtr — o'sha sahifa. Usiz router uzun ro'yxatni tepaga sakratishi mumkin edi (aylantirish — keyingi darsda).

App.tsx da bitta o'zgarish: filtrniYoz, saralashniYoz endi hook'dan keladi.

6.4 Natija

Xuddi o'sha besh qadam, tuzatishdan keyin (natijalar/09-tarix-keyin.txt, aynan):

text
ochildi                            /  history.state={"idx":0}
filtr «Faol» bosildi               /?filtr=faol  history.state={"usr":null,"key":"urthxv9q","idx":1}
saralash «alifbo»                  /?filtr=faol&saralash=alifbo  history.state={"usr":null,"key":"cdn42fo1","idx":2}
«Batafsil» (router)                /vazifa/1  history.state={"usr":null,"key":"fuwaevq4","idx":3}
Orqaga (popstate)                  /?filtr=faol&saralash=alifbo  history.state={"usr":null,"key":"cdn42fo1","idx":2}

idx — 0, 1, 2, 3: router hisobi brauzer tarixi bilan bir xil. «Orqaga» dan keyin router o'sha kalitni (cdn42fo1) taniydi.

bash
git add -A
git commit

Commit (kanon): refactor: filtr va saralash — useSearchParams (tarixning egasi — router).

  • npm test — node:test 181 (o'z pushState testlari parametrniQoy testlariga almashdi; 04-dars tuzatishi bilan), Vitest 56. Vitest'da App endi BrowserRouter ichida chiziladi — testlar location.search ni tekshiradi.
  • Brauzer: 159/159, 72/72, 19-qism — 52/52. Yangilaridan (aynan):
text
✅ o'sha filtr qayta bosildi — tarix o'smadi: [0,2]
✅ «Batafsil»: idx 3 (router hisobi tarix bilan bir xil): 3
✅ Orqaga ×2: filtr faol, saralash standart, ro'yxat ergashdi: [["?filtr=faol",1,"string"],"standart",["1","3"]]

Diff: 5 fayl, +114 −118 — kod qisqardi. Sahifalash vazifalar da yo'q: ro'yxat virtual (18-qism), API'da sahifa tushunchasi yo'q.

7. Hujumchi nigohi

URL'dagi qiymat — hujumchi to'liq boshqaradigan kirish. Uch xavf:

  • Tekshirilmagan qiymat — ilova ichiga. ?saralash=__proto__, ?turi=<img onerror=…>. Oq ro'yxat (parametrniOl) faqat ma'lum qiymatlarni o'tkazadi — qolgani standart. React matnni escape qiladi, lekin qiymat dangerouslySetInnerHTML, href, style yoki obyekt kaliti (obj[saralash]) ga tushsa — xavf (Frontend xavfsizligi).
  • Server so'roviga to'g'ridan. fetch(\/api/taom?saralash=${q}`)—qda&narx=0bo'lsa, so'rovga yangi parametr qo'shiladi. Har doimURLSearchParams` bilan yasang — u qiymatni kodlaydi.
  • Resurs hujumi. ?hajm=1000000 — "bir sahifada million taom". Son parametrlarni chegaralang (Math.min), server ham chegaralaydi.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
?turi=pitsa — bo'sh ro'yxat qiymat tekshirilmagan oq ro'yxat, begona — standart
filtr bosilsa, qidiruv yo'qoldi setSearchParams({ … }) hammasini almashtirdi eski parametrlardan nusxa (new URLSearchParams(eski))
?filtr=hammasi va ? — ikki manzil standart qiymat yozildi standartga teng — delete
«Orqaga» ni ko'p bosish kerak har harf — push replace + debounce
o'sha tugma qayta bosilsa, «Orqaga» "ishlamaydi" URL o'zgarmasa ham navigatsiya toString() solishtiruvi
?sahifa=abc → NaN, ?sahifa=999 → bo'sh son tekshirilmagan Number.isInteger, Math.min(n, last)
router tarixi adashdi (state=null) o'z history.pushState URL'ni faqat router orqali o'zgartiring
filtrdan keyin sahifa tepaga sakradi aylantirish tiklandi preventScrollReset: true

9. Mashqlar

Mashqlar kurs/mashqlar/19/09-url-holat/ da. Har yechimni «Ishga tushir» bilan sinang.

1-mashq (oson): URL yoki useState?

Har holat uchun joyni yozing (URL yoki useState):

  1. Menyuda tanlangan saralash («arzonidan») →
  2. "Buyurtma berish" modalining ochiq/yopiqligi →
  3. Qidiruv maydoniga hozir yozilayotgan harflar →
  4. Buyurtmalar ro'yxatining 3-sahifasi →
Yechim

Birinchi va to'rtinchi — URL: havolani ulashganda boshqa odam ham aynan shuni ko'rishi kerak. Ikkinchisi — useState: modal vaqtinchalik, havolada ochiq modal kutilmaydi. Uchinchisi — useState, lekin debounce bilan URL'ga ham ketadi: yozilayotgan har harf — useState, tugagan qidiruv — URL. Savol doim bitta: "Bu havola bilan birga ketishi kerakmi?"

2-mashq (o'rta): standartni tozalang

Quyidagi saralash tugmalari standart qiymatni ham URL'ga yozadi: «Standart» bosilsa — ?saralash=standart. Tuzating: standart bo'lsa, saralash URL'dan o'chsin. turi saqlansin.

Ishora: «Oq ro'yxat bilan o'qish» bo'limidagi setParam.

jsx
import {
  createMemoryRouter,
  RouterProvider,
  useLocation,
  useSearchParams,
} from "react-router";

function Sort() {
  const { search } = useLocation();
  const [params, setParams] = useSearchParams();
  function choose(value) {
    const next = new URLSearchParams(params);
    next.set("saralash", value);
    setParams(next);
  }
  return (
    <section>
      <p>URL: {search || "(bo'sh)"}</p>
      <button onClick={() => choose("standart")}>
        Standart
      </button>{" "}
      <button onClick={() => choose("arzon")}>Arzonidan</button>
    </section>
  );
}

const router = createMemoryRouter(
  [{ path: "/menyu", element: <Sort /> }],
  { initialEntries: ["/menyu?turi=issiq"] },
);

export default function App() {
  return <RouterProvider router={router} />;
}

Ekranda:

text
URL: ?turi=issiq&saralash=standart
Standart Arzonidan
Yechim
jsx
import {
  createMemoryRouter,
  RouterProvider,
  useLocation,
  useSearchParams,
} from "react-router";

function Sort() {
  const { search } = useLocation();
  const [params, setParams] = useSearchParams();
  function choose(value) {
    const next = new URLSearchParams(params);
    if (value === "standart") next.delete("saralash");
    else next.set("saralash", value);
    if (next.toString() !== params.toString()) setParams(next);
  }
  return (
    <section>
      <p>URL: {search || "(bo'sh)"}</p>
      <button onClick={() => choose("standart")}>
        Standart
      </button>{" "}
      <button onClick={() => choose("arzon")}>Arzonidan</button>
    </section>
  );
}

const router = createMemoryRouter(
  [{ path: "/menyu", element: <Sort /> }],
  { initialEntries: ["/menyu?turi=issiq"] },
);

export default function App() {
  return <RouterProvider router={router} />;
}

Ekranda:

text
URL: ?turi=issiq
Standart Arzonidan

Oraliq holatda ?turi=issiq&saralash=arzon, oxirida — yana ?turi=issiq: ochilgandagi manzil bilan bir xil. Qo'shimcha: toString() solishtiruvi — «Standart» ikki marta bosilsa, tarix o'smaydi.

3-mashq (qiyin): manzilni tuzatish

«Sahifalash URL'da» bo'limidagi misolni o'zgartiring: ?sahifa= yaroqsiz yoki chegaradan tashqarida bo'lsa (999, abc, 0), komponent to'g'ri sahifani ko'rsatish bilan birga manzilni ham tuzatsin — replace bilan. 1-sahifa bo'lsa, sahifa URL'dan o'chsin.

Ishora: 04-darsdagi <Navigate replace> — kanonik manzil.

Yechim
jsx
import {
  createMemoryRouter,
  Navigate,
  RouterProvider,
  useLocation,
  useSearchParams,
} from "react-router";

const DISHES = ["Osh", "Manti", "Lag'mon", "Somsa", "Chuchvara"];
const SIZE = 2;

function readPage(params, last) {
  const n = Number(params.get("sahifa") ?? "1");
  if (!Number.isInteger(n) || n < 1) return 1;
  return Math.min(n, last);
}

function Menu() {
  const { pathname, search } = useLocation();
  const [params] = useSearchParams();
  const last = Math.ceil(DISHES.length / SIZE);
  const page = readPage(params, last);
  // Kanonik qiymat: 1 — yo'q, boshqasi — satr
  const wanted = page === 1 ? null : String(page);
  if (params.get("sahifa") !== wanted) {
    const next = new URLSearchParams(params);
    if (wanted === null) next.delete("sahifa");
    else next.set("sahifa", wanted);
    const s = next.toString();
    return <Navigate to={pathname + (s ? `?${s}` : "")} replace />;
  }
  const items = DISHES.slice((page - 1) * SIZE, page * SIZE);
  return (
    <section>
      <p>URL: {search || "(bo'sh)"}</p>
      <p>
        {page}-sahifa: {items.join(", ")}
      </p>
    </section>
  );
}

const router = createMemoryRouter(
  [{ path: "/menyu", element: <Menu /> }],
  { initialEntries: ["/menyu?sahifa=999&turi=issiq"] },
);

export default function App() {
  return <RouterProvider router={router} />;
}

Ekranda:

text
URL: ?sahifa=3&turi=issiq
3-sahifa: Chuchvara

params.get("sahifa") URL'dagi xom qiymat, wanted — bo'lishi kerak bo'lgani. Farq qilsa — <Navigate replace>: tarix o'smaydi, «Orqaga» ?sahifa=999 ga qaytarmaydi. initialEntries ni ?sahifa=abc yoki ?sahifa=1 ga almashtirib sinang — ikkalasida ham URL ?turi=issiq bo'ladi.

4-mashq: Vazifalar qadami

refactor/url-router branch'ida «Vazifalar qadami» dagi o'zgarishlarni qiling: marshrut.ts — sof funksiyalar, useUrlTanlovi — useSearchParams, App.tsx — yozish hook'dan, testlar (parametrniQoy, Vitest'da BrowserRouter). Keyin commit.

Yechim

Tekshiruv ro'yxati (npm run dev, DevTools konsolida history.state):

  • «Faol» — manzil ?filtr=faol, history.state da key va idx: 1.
  • «Faol» ni yana bosing — history.length o'zgarmaydi.
  • «Hammasi» — filtr URL'dan o'chadi.
  • Filtr, saralash, «Batafsil», keyin «Orqaga» ikki marta — avval saralash, keyin filtr qaytadi, ro'yxat ergashadi.
  • Uzun ro'yxatni pastga aylantirib, filtr bosing — sahifa tepaga sakramaydi.
  • marshrut.ts da history.pushState qolmagan: grep pushState src — bo'sh.
  • npm test — node 181, Vitest 56.

10. Real ishda

  • Internet-do'konlar — Uzum, Ozon, Amazon filtrlari URL'da: ?narx=0-100000&rang=qizil&sahifa=2. Filtrlangan sahifani ulashish va Google indeksi shunga tayanadi.
  • Admin jadvallar — ?saralash=-sana&sahifa=3&hajm=50. TanStack Table + URL holati — ko'p uchraydigan juftlik.
  • nuqs — Next.js va React Router loyihalarida keng tarqalgan; TanStack Router'da validateSearch (oldingi dars) shu vazifani bajaradi.
  • Intervyu savollari: "filtrni state'da yoki URL'da saqlaysizmi va nega?", "qidiruvda har harf tarixga yozilmasligi uchun nima qilasiz?", "URL'dagi qiymatga ishonsa bo'ladimi?".

Xulosa

  • Ulashiladigan holat (filtr, saralash, qidiruv, sahifa) — URL'da; vaqtinchalik UI — useState.
  • O'qish — oq ro'yxat bilan, begona qiymat — standart. Son — Number.isInteger va chegara.
  • Yozish: standart qiymat URL'dan o'chadi, boshqa parametrlar saqlanadi, o'zgarmagan URL navigatsiya qilmaydi.
  • Tugma — push, yozish — replace + debounce (300 ms).
  • Tarixning egasi bitta: router bor joyda o'z pushState yo'q (kanon: state=null, idx adashdi).
  • nuqs — parser'lar, withDefault, history sozlamasi bilan shu qoidalarni bitta hook'ga yig'adi.

Keyingi dars: Scroll, breadcrumb va sahifa o'tishlari — sahifa almashganda aylantirish va fokus qayerda bo'ladi, non yo'li marshrutlardan qanday quriladi va View Transitions bilan silliq o'tish.

Manbalar

  • React Router: useSearchParams (preventScrollReset, replace) — reactrouter.com/api/hooks/useSearchParams (2026-10 holatiga, v8.4.0)
  • nuqs: Basic usage, Parsers, Options, Adapters — nuqs.dev; npm view nuqs version peerDependencies exports (2.10.2, 2026-10-14)
  • MDN: History.state, History.pushState(), URLSearchParams — developer.mozilla.org
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
URL holat sifatida: filtr, saralash va sahifalash — useSearchParams, debounce, nuqs — IlmHamroh