IlmHamroh
JavaScript Full-stack/19-qism. React: routing, server state, formalar10/36-dars18 daqiqa
Mundarija (29)

React Router'da scroll, breadcrumb va sahifa o'tishlari: ScrollRestoration, useMatches, viewTransition

Qisqacha: SPA'da sahifa almashganda brauzer aylantirishni (scroll) o'zi boshqarmaydi — uni router yoki siz hal qilasiz. Masalan, <ScrollRestoration /> yangi sahifani tepadan ochadi va «Orqaga» da eski joyni tiklaydi; preventScrollReset — filtr kabi o'sha sahifadagi o'zgarishda joyni saqlaydi. Fokus ham yangi sahifaga — odatda sarlavhaga — o'tkaziladi, aks holda ekran o'quvchi yangi sahifani "eshitmaydi". Breadcrumb (non yo'li) — useMatches() dagi har marshrutning handle maydonidan quriladi. viewTransition prop'i o'tishni brauzerning View Transitions API'si bilan silliq qiladi.

Bu darsda

  • SPA'da aylantirish nega o'z-o'zidan to'g'ri ishlamasligini va <ScrollRestoration>, preventScrollReset, hash havolalar qanday yordam berishini bilasiz.
  • Sahifa almashganda fokusni yangi sarlavhaga o'tkazasiz.
  • handle va useMatches() bilan marshrutlardan avtomatik non yo'li (breadcrumb) qurasiz.
  • viewTransition bilan sahifa o'tishini silliq qilasiz va prefers-reduced-motion ni hurmat qilasiz.
  • Kanon o'lchovini o'qib, nega vazifalar ga <ScrollRestoration> kerak bo'lmaganini tushuntirasiz.

Oldin bilishingiz kerak: URL — holat saqlash joyi, data mode — loader, Fokus boshqaruvi.

1. Nega bu kerak?

Oddiy (ko'p sahifali) saytda brauzer uch narsani o'zi qiladi. Havola bosilsa — yangi sahifa tepadan ochiladi. «Orqaga» bosilsa — eski sahifa o'sha joyidan ko'rinadi. Fokus esa hujjatning boshiga qaytadi va ekran o'quvchi yangi sahifa sarlavhasini o'qiydi.

SPA'da "yangi sahifa" yo'q — o'sha hujjat ichida React kontentni almashtiradi. Natija:

  1. «Bahor» menyusining pastida «Manti» ni bosdingiz. Manti sahifasi ochildi, lekin pastdan — sahifa 1500 px aylantirilgan holda qoldi.
  2. Fokus bosilgan havolada edi. Havola yo'qoldi — fokus <body> ga tushdi. Malika (ekran o'quvchi) hech narsa eshitmadi; Bobur Tab bosdi — fokus sahifaning eng boshidan boshlandi.
  3. Vazifa sahifasida "qayerdaman?" degan savolga javob yo'q: «Ro'yxat / Sut olish» kabi yo'l ko'rsatkichi yo'q.

Bugun uchalasini ham hal qilamiz va bonus — sahifalar orasida silliq o'tish.

Hayotiy o'xshatish — kitob. Yangi bobni ochsangiz, birinchi sahifadan boshlaysiz (scroll tepaga). Oldingi bobga qaytsangiz, xatcho'p qo'ygan joyingizni topasiz (scroll tiklash). Har sahifaning tepasida "3-qism › 2-bob" deb yozilgan (non yo'li).

2. Aylantirish (scroll)

2.1 <ScrollRestoration />

React Router'ning data router'larida (createBrowserRouter) tayyor komponent bor. Uni ildiz layout'ga bir marta qo'yasiz:

jsx
function Shell() {
  return (
    <>
      <Header />
      <Outlet />
      <ScrollRestoration />
    </>
  );
}

U nimani qiladi:

Holat Natija
yangi sahifaga o'tish (PUSH, REPLACE) tepaga (scrollY = 0)
«Orqaga»/«Oldinga» (POP) shu yozuv uchun eslab qolingan joy
manzilda #bo'lim bor o'sha id li elementga
preventScrollReset bilan o'tish joy o'zgarmaydi

Joylarni u sessionStorage da, har tarix yozuvining key i bo'yicha saqlaydi. 04-darsdagi location.key ning yana bir ishi.

Natija oynasi sahifani aylantira olmaydi (u kichik iframe), shuning uchun bu bo'limda jonli blok yo'q. Aylantirishni kanon Chrome'da o'lchagan — «Vazifalar qadami» da.

2.2 Qachon tepaga emas: preventScrollReset

Filtr, saralash, sahifa ichidagi tab — yangi "sahifa" emas. Foydalanuvchi ro'yxatning o'rtasida «Faol» ni bosdi va sahifa tepaga sakradi — yoqimsiz. URL holati darsida vazifalar filtri shuning uchun preventScrollReset: true bilan yoziladi:

jsx
setSearchParams(yangi, { preventScrollReset: true });
<Link to="?filtr=faol" preventScrollReset>Faol</Link>

2.3 Hash havola: sahifa ichidagi joy

Menyuning uzun sahifasida bo'limlar: «Issiq taomlar», «Ichimliklar». Ularga havola — # bilan: <Link to="#ichimliklar">. HTML'dan bilasiz (Havola asoslari): brauzer id="ichimliklar" elementga aylantiradi. SPA'da buni <ScrollRestoration> qiladi — manzil o'zgargach, location.hash ga mos elementni topadi. U yo'q bo'lsa — o'zingiz, effektda:

jsx
const { hash } = useLocation();
useEffect(() => {
  if (hash) {
    document.getElementById(decodeURIComponent(hash.slice(1)))
      ?.scrollIntoView();
  }
}, [hash]);

decodeURIComponent — o'zbekcha id (#ko'k-choy) URL'da kodlangan bo'lishi mumkin.

Tekshirib ko'ring: Foydalanuvchi menyu sahifasini 2000 px pastga aylantirdi, «Osh» ni bosdi (yangi sahifa), keyin «Orqaga». <ScrollRestoration /> bo'lsa, ikkala o'tishdan keyin scrollY qancha?

Javob

Osh sahifasida — 0 (yangi yozuv, PUSH). «Orqaga» dan keyin — 2000: menyuning yozuvi uchun eslab qolingan joy (POP). Agar ro'yxat ma'lumoti hali kelmagan bo'lsa (sahifa balandligi qisqa), router tiklashni loader tugashini kutib bajaradi — data router'ning afzalligi.

3. Fokus yangi sahifaga

3.1 Muammo: fokus <body> da

Klaviatura va ekran o'quvchi uchun "sahifa almashdi" — bu fokus bilan bildiriladi. Ko'p sahifali saytda brauzer yangi hujjatni o'qiydi. SPA'da hech narsa bo'lmaydi: bosilgan havola o'chdi, fokus <body> ga tushdi. Konsolda ko'ramiz:

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

function Shell() {
  const { pathname } = useLocation();
  useEffect(() => {
    const el = document.activeElement;
    console.log(pathname, "→ fokus:", el.tagName);
  }, [pathname]);
  return <Outlet />;
}

const router = createMemoryRouter([
  {
    path: "/",
    element: <Shell />,
    children: [
      { index: true, element: <Link to="/taom/manti">Manti</Link> },
      { path: "taom/:slug", element: <h2>Manti</h2> },
    ],
  },
]);

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

Konsolda:

text
/ → fokus: BODY
/taom/manti → fokus: BODY

Ikkinchi qator: sahifa almashdi, fokus — BODY. (Tekshiruvchi bos qadamida havolaga fokus qo'ymaydi, shuning uchun birinchi qatorda ham BODY; brauzerda bosilgan havola fokusda bo'ladi, keyin u o'chib, fokus BODY ga tushadi.)

3.2 Davosi: sarlavhaga fokus

Yangi sahifaning sarlavhasiga tabIndex={-1} beriladi (dasturiy fokus olishi uchun, Tab tartibiga kirmaydi) va yo'l o'zgarganda fokus unga o'tkaziladi (Fokus boshqaruvi):

jsx
import { useEffect, useRef } from "react";
import {
  createMemoryRouter,
  Link,
  Outlet,
  RouterProvider,
  useLocation,
  useOutletContext,
} from "react-router";

function Shell() {
  const { pathname } = useLocation();
  const titleRef = useRef(null);
  const prevPath = useRef(pathname);
  useEffect(() => {
    // Birinchi ochilishda emas — faqat yo'l o'zgarganda
    if (prevPath.current !== pathname) {
      prevPath.current = pathname;
      titleRef.current?.focus({ preventScroll: true });
      const el = document.activeElement;
      console.log(pathname, "→ fokus:", el.tagName, el.textContent);
    }
  }, [pathname]);
  return <Outlet context={titleRef} />;
}

function Dish() {
  // Outlet konteksti — sarlavha ref'i
  const titleRef = useOutletContext();
  return (
    <h2 ref={titleRef} tabIndex={-1}>
      Manti
    </h2>
  );
}

const router = createMemoryRouter([
  {
    path: "/",
    element: <Shell />,
    children: [
      { index: true, element: <Link to="/taom/manti">Manti</Link> },
      { path: "taom/:slug", element: <Dish /> },
    ],
  },
]);

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

Konsolda:

text
/taom/manti → fokus: H2 Manti

Fokus — sarlavhada. Malika "Manti, sarlavha 2-daraja" ni eshitadi, Bobur Tab bosganda sahifa mazmuni bo'ylab davom etadi. Uch detal:

  • Birinchi ochilishda fokus qo'yilmaydi — sahifa yuklanganda brauzer o'zi boshidan boshlaydi, fokusni "o'g'irlash" kerak emas.
  • preventScroll: true — fokus aylantirishni o'zgartirmasin; uni <ScrollRestoration> (yoki brauzer) boshqaradi.
  • Ref layout'da, sarlavha sahifada — <Outlet context> orqali (03-dars). vazifalar da bu sarlavhaRef — 19/#02 dan beri skip link uchun bor edi.

4. Non yo'li (breadcrumb)

4.1 handle va useMatches

Non yo'li (breadcrumb) — sahifa tepasidagi "Ro'yxat / Sut olish" kabi yo'l. Nomi ertakdan: bolalar o'rmonda adashmaslik uchun non ushoqlarini tashlab ketgan.

Uni qo'lda yozish — har sahifada "men qayerdaman" ni takrorlash. Router esa buni allaqachon biladi: /taom/osh/sharhlar ochilganda mos kelgan marshrutlar zanjiri — ildiz, taom, sharhlar. useMatches() shu zanjirni beradi. Har marshrut o'z nomini handle maydonida aytadi — router unga tegmaydi, bu siz uchun "ixtiyoriy ma'lumot" joyi:

jsx
import {
  createMemoryRouter,
  Link,
  Outlet,
  RouterProvider,
  useMatches,
} from "react-router";

function Breadcrumbs() {
  const crumbs = useMatches()
    .filter((m) => m.handle?.crumb)
    .map((m) => ({
      path: m.pathname,
      name: m.handle.crumb(m.loaderData),
    }));
  return (
    <nav aria-label="Non yo'li">
      {crumbs.map((c, i) =>
        i === crumbs.length - 1 ? (
          <span key={c.path} aria-current="page">
            {c.name}
          </span>
        ) : (
          <span key={c.path}>
            <Link to={c.path}>{c.name}</Link> /{" "}
          </span>
        ),
      )}
    </nav>
  );
}

function Shell() {
  return (
    <div>
      <Breadcrumbs />
      <Outlet />
    </div>
  );
}

const router = createMemoryRouter([
  {
    path: "/",
    element: <Shell />,
    handle: { crumb: () => "Menyu" },
    children: [
      {
        index: true,
        element: <Link to="/taom/osh/sharhlar">Osh sharhlari</Link>,
      },
      {
        path: "taom/:slug",
        element: <Outlet />,
        // nom — loader ma'lumotidan
        loader: () => ({ name: "Osh" }),
        handle: { crumb: (data) => data?.name ?? "Taom" },
        children: [
          {
            path: "sharhlar",
            element: <p>3 ta sharh</p>,
            handle: { crumb: () => "Sharhlar" },
          },
        ],
      },
    ],
  },
]);

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

Ekranda:

text
Menyu / Osh / Sharhlar
3 ta sharh

Zanjir: / → /taom/osh → /taom/osh/sharhlar. Har birining handle.crumb i o'z nomini berdi. Taomning nomi — loader ma'lumotidan (m.loaderData): URL'da osh, ekranda «Osh». Oxirgi bo'lak havola emas va aria-current="page" — Malika "joriy sahifa" deb eshitadi.

flowchart LR
  U["/taom/osh/sharhlar"] --> M["useMatches()"]
  M --> A["/ <br/>handle: Menyu"]
  M --> B["/taom/osh<br/>handle: loaderData.name"]
  M --> C["/taom/osh/sharhlar<br/>handle: Sharhlar"]
  A & B & C --> N["Menyu / Osh / Sharhlar"]

Asosiy afzallik: Breadcrumbs komponenti marshrutlarni bilmaydi. Yangi sahifa qo'shilsa, unga handle yoziladi — non yo'li o'zi yangilanadi.

4.2 Semantika

Ekranda / bilan ajratilgan qator. HTML'da esa tartibli ro'yxat: <nav aria-label="…"><ol><li>…</li></ol></nav>, ajratuvchi / — CSS'da (li + li::before { content: "/" }). Shunda ekran o'quvchi "ro'yxat, 3 ta element" deydi va ajratuvchini o'qimaydi. vazifalar dagi NonYoli aynan shunday (pastda).

Tekshirib ko'ring: taom/:slug marshruti errorElement ga tushdi (loader 404 tashladi). crumb(data) ga nima keladi va non yo'lida nima yoziladi?

Javob

Loader xato tashladi — ma'lumot yo'q, m.loaderData — undefined. data?.name ?? "Taom" aynan shu uchun: non yo'lida "Menyu / Taom". Zaxirasiz data.name non yo'lini ham, butun sahifani ham yiqitardi. vazifalar dagi vazifaNomi ham zaxira bilan: ?.matn ?? "Vazifa".

5. Sahifa o'tishlari: viewTransition

5.1 View Transitions API

Brauzerning View Transitions API sini CSS qismidan bilasiz (View Transitions). U ikki holat orasida silliq o'tish beradi: document.startViewTransition(() => o'zgarish). Brauzer eski ko'rinishni "suratga" oladi, o'zgarishni bajaradi va ikkalasi orasida animatsiya chizadi (sukut — yumshoq almashish). React Router'da buni bitta prop yoqadi:

jsx
import {
  createMemoryRouter,
  NavLink,
  Outlet,
  RouterProvider,
  useViewTransitionState,
} from "react-router";

function Shell() {
  return (
    <div>
      <nav>
        <NavLink to="/" end viewTransition>
          Ro'yxat
        </NavLink>{" "}
        <NavLink to="/statistika" viewTransition>
          Statistika
        </NavLink>
      </nav>
      <Outlet />
    </div>
  );
}

function Stats() {
  // shu manzilga o'tish animatsiyasi ketyaptimi
  const animating = useViewTransitionState("/statistika");
  return <p>Statistika: 3 ta vazifa{animating ? " (o'tish)" : ""}</p>;
}

const router = createMemoryRouter([
  {
    path: "/",
    element: <Shell />,
    children: [
      { index: true, element: <p>Ro'yxat sahifasi</p> },
      { path: "statistika", element: <Stats /> },
    ],
  },
]);

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

«Ishga tushir» ni bosing va «Statistika» havolasini bosing: Chrome'da sahifa bir lahzalik "yumshoq almashish" bilan o'zgaradi, ekranda — "Statistika: 3 ta vazifa". (Bu blok avtomatik tekshirilmagan: tekshiruvchi muhitida View Transitions API yo'q, router u yerda dev ogohlantirishi beradi.) API'ni qo'llamaydigan brauzerda prop shunchaki e'tiborsiz qoladi — oddiy o'tish (dev rejimida konsolda ogohlantirish). Bu progressiv yaxshilash: bor joyda chiroyli, yo'q joyda buzilmaydi.

useViewTransitionState(manzil) — o'tish paytida true. U bilan bitta elementga nom berib (style={{ viewTransitionName: "rasm" }}) "rasm kichik kartadan katta sahifaga uchib o'tadi" kabi effekt yasaladi.

5.2 CSS va harakat sezgirligi

Animatsiya CSS'da sozlanadi (kanondan aynan):

css
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.12s;
}
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none;
  }
}

::view-transition-old(root) — eski sahifa "surati", new — yangisi. 0.12 s — sezilarli, lekin kutdirmaydi. prefers-reduced-motion: reduce — tizimda "harakatni kamaytirish" yoqilgan foydalanuvchiga animatsiya yo'q (prefers-reduced-motion). Ba'zi odamlarda harakat bosh aylanishi chaqiradi.

Diqqat: O'tish paytida (120 ms) sahifa "surat" — bosish unga yetmaydi. Kanon e2e tekshiruvi buni topdi: animatsiya paytida bosilgan tugma ishlamadi. Shuning uchun vazifalar da viewTransition faqat bo'limlar menyusida — ro'yxatdagi tez-tez bosiladigan havolalarda yo'q.

6. Vazifalar qadami: non yo'li, fokus va o'tishlar (holat 10)

6.1 Kanon o'lchovi: aylantirish

<ScrollRestoration> kerakmi? Kanon taxmin qilmadi — o'lchadi (natijalar/10-aylantirish.txt, Chrome 155, 150 vazifa, ro'yxat 1500 px pastga aylantirilgan; aynan, fokus ustuni qisqartirilgan):

text
--- OLDIN (holat 09)
ro'yxat, 1500 px pastda                  /  scrollY=1500  fokus=body
«Batafsil» (vazifa 12)                   /vazifa/12  scrollY=0  fokus=body
Orqaga                                   /  scrollY=1500  fokus=body
--- KEYIN (holat 10: fokus sarlavhaga, ScrollRestoration'siz)
ro'yxat, 1500 px pastda                  /  scrollY=1500  fokus=body
«Batafsil» (vazifa 12)                   /vazifa/12  scrollY=0  fokus=h2|Vazifa 12
Orqaga                                   /  scrollY=1500  fokus=h2|Ro'yxat

Aylantirish holat 09 da ham to'g'ri edi: vazifa sahifasi tepadan (0), «Orqaga» — o'sha 1500 px. Nega? Vazifa sahifasi qisqa — brauzer sahifa balandligi kichrayganda scrollY ni o'zi 0 ga tushiradi. Ro'yxat esa keshdan sinxron chiziladi: «Orqaga» bosilgan zahoti sahifa to'liq balandlikda, brauzerning o'z tiklash mexanizmi ulgurdi. <ScrollRestoration> — kirmadi: qo'shimcha kod, foyda yo'q.

Haqiqiy muammo — fokus ustuni: hamma joyda body. Tuzatish — sarlavhaga.

bash
git switch -c feat/non-yoli-fokus

6.2 Fokus — Qobiq.tsx

«Davosi: sarlavhaga fokus» bo'limidagi kod, bitta istisno bilan (kanondan aynan):

tsx
  const { pathname } = useLocation();
  const oldingiYol = useRef(pathname);
  useEffect(() => {
    if (oldingiYol.current !== pathname) {
      // Kirishdan keyin — fokusni foydalanuvchi menyusi o'zi qo'yadi
      // (18/#42: "Malika" tugmasi — kim kirgani eshitiladi)
      const kirishdan = oldingiYol.current === "/kirish";
      oldingiYol.current = pathname;
      if (!kirishdan) {
        sarlavhaRef.current?.focus({ preventScroll: true });
      }
    }
  }, [pathname]);

pathname ga bog'liq, search ga emas: filtr — o'sha sahifa, fokus filtr tugmasida qolishi kerak. Istisno — /kirish dan kelganda: u holda 18-qismdagi foydalanuvchi menyusi fokusni o'ziga oladi.

6.3 Non yo'li — NonYoli.tsx va handle

marshrutlar.tsx da har bo'lim o'z nomini aytadi (kanon):

tsx
// Non yo'li bo'laklari (19/#10): Qobiq ularni useMatches() bilan
// yig'adi
const nomi = (matn: string): NonYoliHandle => ({
  nonYoli: () => matn,
});
const vazifaNomi: NonYoliHandle = {
  // loader ma'lumoti — vazifa (xato bo'lsa undefined)
  nonYoli: (malumot) =>
    (malumot as VazifaMalumoti | undefined)?.matn ?? "Vazifa",
};
// … Himoyalangan: handle: nomi("Ro'yxat"),
//     statistika: handle: nomi("Statistika"),
//     vazifa/:id: handle: vazifaNomi

NonYoli.tsx — «handle va useMatches» bo'limidagi Breadcrumbs ning TypeScript varianti (kanondan, qisqartirilgan):

tsx
function nonYolimi(handle: unknown): handle is NonYoliHandle {
  return (
    typeof handle === "object" &&
    handle !== null &&
    "nonYoli" in handle &&
    typeof handle.nonYoli === "function"
  );
}

export function NonYoli() {
  const bolaklar = useMatches().flatMap((m: UIMatch) =>
    nonYolimi(m.handle)
      ? [{ yol: m.pathname, nomi: m.handle.nonYoli(m.loaderData) }]
      : [],
  );
  if (bolaklar.length < 2) {
    return null;
  }
  // <nav aria-label="Non yo'li"><ol>… oxirgisi aria-current="page"
  • handle ning turi unknown — router unga nima yozilganini bilmaydi. nonYolimi — type guard: tekshiruvdan keyin TypeScript m.handle.nonYoli ni taniydi.
  • 2 bo'lakdan kam — chizilmaydi: ro'yxat sahifasida "Ro'yxat" yozuvining o'zi ma'nosiz.
  • Qobiq da: {kirgan && <NonYoli />} — kirish sahifasida yo'q.

6.4 O'tishlar va tekshiruvlar

Bo'limlar menyusidagi uch NavLink ga viewTransition, vazifalar.css ga «CSS va harakat sezgirligi» bo'limidagi qoidalar va non yo'li uslubi.

bash
git add -A
git commit

Commit (kanon): feat: non yo'li (handle + useMatches), fokus yangi sahifaga, View Transitions.

  • npm test — node:test 181, Vitest 56 (bitta test 1 s ga sig'madi — 3 s ga oshirildi).
  • Brauzer: 159/159, 72/72, 19-qism — 59/59. Yangilaridan (aynan):
text
✅ non yo'li: ro'yxat sahifasida yo'q: []
✅ NavLink: fokus yangi sahifa sarlavhasida, View Transition bilan: ["Statistika",1]
✅ non yo'li: Ro'yxat / Statistika (joriy): ["Ro'yxat","Statistika*"]
✅ «Batafsil»: fokus vazifa sarlavhasida; o'tish yo'q (faqat menyuda): ["Sut olish #bozor",1]
✅ non yo'li: Ro'yxat / vazifa matni (loader ma'lumotidan): ["Ro'yxat","Sut olish #bozor*"]
✅ reduced-motion: ::view-transition-old(root) animatsiyasi yo'q: "none"

* — joriy bo'lak (aria-current). Beshinchi qatorda vazifa matni loader'dan keldi. Oxirgisi — "harakatni kamaytirish" yoqilgan brauzerda animatsiya none. Diff: 5 fayl, +134 −10.

7. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
yangi sahifa pastdan ochiladi SPA'da aylantirish tiklanmaydi <ScrollRestoration />
filtr bosilsa, sahifa tepaga sakraydi o'sha sahifadagi o'zgarish ham "yangi sahifa" deb tiklandi preventScrollReset
ekran o'quvchi yangi sahifani o'qimaydi fokus <body> da sarlavhaga tabIndex={-1} va focus()
sahifa ochilishi bilan fokus sakraydi birinchi chizishda ham focus() faqat yo'l o'zgarganda
non yo'lida undefined handle loader ma'lumotiga ishondi data?.name ?? "zaxira"
animatsiya paytida bosish yo'qoldi View Transition — "surat" viewTransition faqat kam bosiladigan joyda
"harakatni kamaytirish" da ham animatsiya prefers-reduced-motion yo'q animation: none

8. Mashqlar

Mashqlar kurs/mashqlar/19/10-scroll-nonyoli/ da. Har yechimni «Ishga tushir» bilan sinang.

1-mashq (oson): qaysi vosita?

Har vaziyat uchun vositani yozing:

  1. «Orqaga» da ro'yxat o'sha joyidan ko'rinsin →
  2. Saralash tugmasi bosilganda sahifa tepaga sakramasin →
  3. Non yo'li uchun mos kelgan marshrutlar zanjiri →
  4. Har marshrutning non yo'lidagi nomi qayerda yoziladi →
Yechim

<ScrollRestoration /> — joyni har tarix yozuvi uchun eslab qoladi. preventScrollReset — "bu yangi sahifa emas". useMatches() — zanjir. handle — marshrutdagi ixtiyoriy ma'lumot: router unga tegmaydi, komponentlar o'qiydi. Non yo'lidan tashqari handle da sahifa sarlavhasi, menyu belgisi, "bu sahifa kirishni talab qiladi" kabi narsalar ham saqlanadi.

2-mashq (o'rta): non yo'liga uchinchi qavat

Quyidagi ilovada non yo'li ishlaydi, lekin «Tahrir» sahifasida u faqat "Menyu / Osh" ni ko'rsatadi. Tahrir marshrutiga handle qo'shing — "Menyu / Osh / Tahrir" bo'lsin.

jsx
import {
  createMemoryRouter,
  Link,
  Outlet,
  RouterProvider,
  useMatches,
} from "react-router";

function Breadcrumbs() {
  const names = useMatches()
    .filter((m) => m.handle?.crumb)
    .map((m) => m.handle.crumb);
  return <p>{names.join(" / ")}</p>;
}

const router = createMemoryRouter(
  [
    {
      path: "/",
      element: (
        <div>
          <Breadcrumbs />
          <Outlet />
        </div>
      ),
      handle: { crumb: "Menyu" },
      children: [
        {
          path: "taom/osh",
          element: <Outlet />,
          handle: { crumb: "Osh" },
          children: [
            { index: true, element: <Link to="tahrir">Tahrir</Link> },
            { path: "tahrir", element: <p>Narx: 35 000</p> },
          ],
        },
      ],
    },
  ],
  { initialEntries: ["/taom/osh"] },
);

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

Ekranda:

text
Menyu / Osh
Narx: 35 000
Yechim
jsx
import {
  createMemoryRouter,
  Link,
  Outlet,
  RouterProvider,
  useMatches,
} from "react-router";

function Breadcrumbs() {
  const names = useMatches()
    .filter((m) => m.handle?.crumb)
    .map((m) => m.handle.crumb);
  return <p>{names.join(" / ")}</p>;
}

const router = createMemoryRouter(
  [
    {
      path: "/",
      element: (
        <div>
          <Breadcrumbs />
          <Outlet />
        </div>
      ),
      handle: { crumb: "Menyu" },
      children: [
        {
          path: "taom/osh",
          element: <Outlet />,
          handle: { crumb: "Osh" },
          children: [
            { index: true, element: <Link to="tahrir">Tahrir</Link> },
            {
              path: "tahrir",
              element: <p>Narx: 35 000</p>,
              handle: { crumb: "Tahrir" },
            },
          ],
        },
      ],
    },
  ],
  { initialEntries: ["/taom/osh"] },
);

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

Ekranda:

text
Menyu / Osh / Tahrir
Narx: 35 000

Bitta qator — handle: { crumb: "Tahrir" }. Breadcrumbs o'zgarmadi: u marshrutlarni bilmaydi, faqat zanjirni o'qiydi. Index bolada handle yo'q — u otaning "o'z" sahifasi, alohida bo'lak kerak emas.

3-mashq (qiyin): sarlavha ham handle dan

Brauzer tabidagi sarlavha (<title>) ham sahifaga qarab o'zgarsin: handle.title — eng oxirgi (eng ichki) mos marshrutdagi qiymat, "— «Bahor»" qo'shimchasi bilan. So'ng Shell da <title> ni chizing (React 19 uni <head> ga ko'chiradi) va ekranga ham yozing. /aloqa sahifasida: "Aloqa — «Bahor»".

Ishora: useMatches() massivining oxiridan qidirish — findLast.

Yechim
jsx
import {
  createMemoryRouter,
  Link,
  Outlet,
  RouterProvider,
  useMatches,
} from "react-router";

function Shell() {
  const match = useMatches().findLast((m) => m.handle?.title);
  const title = `${match?.handle.title ?? "Sahifa"} — «Bahor»`;
  return (
    <div>
      <title>{title}</title>
      <p>Tab: {title}</p>
      <Link to="/aloqa">Aloqa</Link>
      <Outlet />
    </div>
  );
}

const router = createMemoryRouter([
  {
    path: "/",
    element: <Shell />,
    handle: { title: "Bosh sahifa" },
    children: [
      {
        path: "aloqa",
        element: <p>Tel: +998 90 000 00 00</p>,
        handle: { title: "Aloqa" },
      },
    ],
  },
]);

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

Ekranda:

text
Tab: Aloqa — «Bahor»
Aloqa
Tel: +998 90 000 00 00

findLast — oxiridan birinchi mos keluvchi: ichki marshrutning sarlavhasi otasinikini "yopadi". Ildiz sahifada (/) faqat bitta mos — "Bosh sahifa". Eslatma: <title> — React 19 da komponent ichida yozish mumkin (Hujjat metadata darsidan tanish), vazifalar sahifalari ham shunday qiladi. handle — bir nechta narsani saqlay oladi: { title, crumb }.

4-mashq: Vazifalar qadami

feat/non-yoli-fokus branch'ida «Vazifalar qadami» bo'limidagi o'zgarishlarni qiling: Qobiq — fokus effekti va <NonYoli />, NonYoli.tsx, marshrutlar.tsx — handle, menyu NavLink larida viewTransition, CSS. Keyin commit.

Yechim

Tekshiruv ro'yxati (npm run dev):

  • Ro'yxatda non yo'li yo'q. «Statistika» — "Ro'yxat / Statistika", fokus "Statistika" sarlavhasida (Tab — keyingi element sahifa ichida).
  • «Batafsil» — "Ro'yxat / "; fokus vazifa sarlavhasida.
  • Uzun ro'yxatni pastga aylantirib «Batafsil», keyin «Orqaga» — o'sha joy.
  • Filtr bosilganda fokus filtr tugmasida qoladi.
  • Tizim sozlamasida "harakatni kamaytirish" (yoki DevTools → Rendering → prefers-reduced-motion: reduce) — menyu o'tishida animatsiya yo'q.
  • npm test — node 181, Vitest 56.

9. Real ishda

  • Har SPA — "sahifa pastdan ochildi" eng ko'p uchraydigan SPA shikoyatlaridan. Next.js va React Router framework mode buni sukut bo'yicha hal qiladi; data mode'da — <ScrollRestoration />.
  • Accessibility auditlari — "SPA'da route o'zgarganda fokus qayerda?" — WCAG auditlarida standart savol. Ba'zi jamoalar fokus o'rniga aria-live e'lon ("Statistika sahifasi ochildi") ishlatadi.
  • Do'konlar va admin panellar — non yo'li: "Bosh / Elektronika / Telefonlar". Google ham uni qidiruv natijasida ko'rsatadi (BreadcrumbList strukturalangan ma'lumot).
  • View Transitions — 2025 dan Chrome, Edge, Safari'da hujjat ichidagi o'tishlar ishlaydi; Next.js va Astro'da ham bir prop bilan yoqiladi.

Xulosa

  • SPA'da aylantirish va fokus o'z-o'zidan to'g'ri bo'lmaydi — router va siz hal qilasiz.
  • <ScrollRestoration /> — yangi sahifa tepadan, «Orqaga» — eslab qolingan joy, #hash — elementga; preventScrollReset — o'sha sahifadagi o'zgarish uchun.
  • Fokus — yangi sahifa sarlavhasiga (tabIndex={-1}, preventScroll), birinchi ochilishda emas.
  • Non yo'li — useMatches() + har marshrutning handle i; nom loader ma'lumotidan bo'lishi mumkin (zaxira bilan).
  • viewTransition — silliq o'tish, prefers-reduced-motion da yo'q; o'tish paytida bosish yo'qolishi mumkin.
  • Kanon o'lchadi: vazifalar da aylantirish brauzerning o'zida to'g'ri edi — <ScrollRestoration> kirmadi, muammo fokusda edi.

Keyingi dars: Server state vs client state — serverdan kelgan ma'lumot nega oddiy state emas, balki kesh ekanini vazifalar dagi so'rovlarni sanab ko'ramiz.

Manbalar

  • React Router: ScrollRestoration, useMatches, handle, viewTransition, useViewTransitionState — reactrouter.com/api/… (2026-10 holatiga, v8.4.0)
  • MDN: View Transition API, prefers-reduced-motion, HTMLElement.focus() (preventScroll) — developer.mozilla.org
  • W3C WAI: Breadcrumb Pattern — w3.org/WAI/ARIA/apg/patterns/breadcrumb
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
React Router'da scroll, breadcrumb va sahifa o'tishlari: ScrollRestoration, useMatches, viewTransition — IlmHamroh