Mundarija (29)
- Bu darsda
- 1. Nega bu kerak?
- 2. Aylantirish (scroll)
- 2.1 <ScrollRestoration />
- 2.2 Qachon tepaga emas: preventScrollReset
- 2.3 Hash havola: sahifa ichidagi joy
- 3. Fokus yangi sahifaga
- 3.1 Muammo: fokus <body> da
- 3.2 Davosi: sarlavhaga fokus
- 4. Non yo'li (breadcrumb)
- 4.1 handle va useMatches
- 4.2 Semantika
- 5. Sahifa o'tishlari: viewTransition
- 5.1 View Transitions API
- 5.2 CSS va harakat sezgirligi
- 6. Vazifalar qadami: non yo'li, fokus va o'tishlar (holat 10)
- 6.1 Kanon o'lchovi: aylantirish
- 6.2 Fokus — Qobiq.tsx
- 6.3 Non yo'li — NonYoli.tsx va handle
- 6.4 O'tishlar va tekshiruvlar
- 7. Ko'p uchraydigan xatolar
- 8. Mashqlar
- 1-mashq (oson): qaysi vosita?
- 2-mashq (o'rta): non yo'liga uchinchi qavat
- 3-mashq (qiyin): sarlavha ham handle dan
- 4-mashq: Vazifalar qadami
- 9. Real ishda
- Xulosa
- Manbalar
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 marshrutninghandlemaydonidan quriladi.viewTransitionprop'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.
handlevauseMatches()bilan marshrutlardan avtomatik non yo'li (breadcrumb) qurasiz.viewTransitionbilan sahifa o'tishini silliq qilasiz vaprefers-reduced-motionni hurmat qilasiz.- Kanon o'lchovini o'qib, nega
vazifalarga<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:
- «Bahor» menyusining pastida «Manti» ni bosdingiz. Manti sahifasi ochildi, lekin pastdan — sahifa 1500 px aylantirilgan holda qoldi.
- 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. - 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:
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:
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:
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 keyinscrollYqancha?
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:
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:
/ → fokus: BODY
/taom/manti → fokus: BODYIkkinchi 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):
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:
/taom/manti → fokus: H2 MantiFokus — 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).vazifalarda busarlavhaRef— 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:
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:
Menyu / Osh / Sharhlar
3 ta sharhZanjir: / → /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/:slugmarshrutierrorElementga 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:
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):
::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
vazifalardaviewTransitionfaqat 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):
--- 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'yxatAylantirish 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.
git switch -c feat/non-yoli-fokus6.2 Fokus — Qobiq.tsx
«Davosi: sarlavhaga fokus» bo'limidagi kod, bitta istisno bilan (kanondan aynan):
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):
// 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: vazifaNomiNonYoli.tsx — «handle va useMatches» bo'limidagi Breadcrumbs ning TypeScript varianti (kanondan, qisqartirilgan):
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"handlening turiunknown— router unga nima yozilganini bilmaydi.nonYolimi— type guard: tekshiruvdan keyin TypeScriptm.handle.nonYolini taniydi.- 2 bo'lakdan kam — chizilmaydi: ro'yxat sahifasida "Ro'yxat" yozuvining o'zi ma'nosiz.
Qobiqda:{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.
git add -A
git commitCommit (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):
✅ 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:
- «Orqaga» da ro'yxat o'sha joyidan ko'rinsin →
- Saralash tugmasi bosilganda sahifa tepaga sakramasin →
- Non yo'li uchun mos kelgan marshrutlar zanjiri →
- 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.
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:
Menyu / Osh
Narx: 35 000Yechim
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:
Menyu / Osh / Tahrir
Narx: 35 000Bitta 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
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:
Tab: Aloqa — «Bahor»
Aloqa
Tel: +998 90 000 00 00findLast — 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-livee'lon ("Statistika sahifasi ochildi") ishlatadi. - Do'konlar va admin panellar — non yo'li: "Bosh / Elektronika / Telefonlar". Google ham uni qidiruv natijasida ko'rsatadi (
BreadcrumbListstrukturalangan 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 marshrutninghandlei; nom loader ma'lumotidan bo'lishi mumkin (zaxira bilan). viewTransition— silliq o'tish,prefers-reduced-motionda yo'q; o'tish paytida bosish yo'qolishi mumkin.- Kanon o'lchadi:
vazifalarda 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
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!