Mundarija (32)
- Bu darsda
- 1. Nega bu kerak?
- 2. To'rt holat
- 2.1 Holatlar xaritasi
- 2.2 Bitta union — imkonsiz holat yo'q
- 2.3 TanStack Query'da xuddi shu to'rtlik
- 3. Yuklanish: qachon va qanday ko'rsatish
- 3.1 Vaqt chegaralari
- 3.2 Kechiktirilgan ko'rsatkich
- 3.3 Kechikish — CSS bilan, JavaScript'siz
- 3.4 Skeleton, spinner yoki progress chizig'i?
- 3.5 Progress chizig'i: navigatsiya paytida
- 4. Bo'sh holat
- 4.1 Ikki xil "bo'sh"
- 4.2 Tuzoq: hidden va display: flex
- 5. Xato holati va qayta urinish
- 5.1 Yaxshi xato xabari
- 5.2 Query bilan qayta urinish
- 5.3 Render xatosi: error boundary bilan
- 6. Suspense bilan: skeleton qayerga qo'yiladi
- 7. Vazifalar qadami: bo'sh holat va kechikadigan skeleton (holat 12)
- 7.1 Nima o'zgaradi
- 7.2 Tekshiruvlar
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): qaysi ko'rsatkich?
- 2-mashq (o'rta): bo'sh va xato aralashib ketgan
- 3-mashq (qiyin): ko'rsatkich + skeleton + eng kam vaqt
- 4-mashq: Vazifalar qadami
- 10. Real ishda
- Xulosa
- Manbalar
Yuklanish, bo'sh va xato holatlari UX React'da: skeleton, kechiktirilgan spinner va qayta urinish
Qisqacha: Ma'lumot ko'rsatadigan har ekranning kamida to'rt holati bor: yuklanmoqda, xato, bo'sh va tayyor. Yuklanishni tez javobda umuman ko'rsatmang — ko'rsatkich 200–300 ms kechikib chiqsin, chiqqan bo'lsa esa bir oz tursin, aks holda ekran miltillaydi. Kontent shakli ma'lum bo'lsa — skeleton, noma'lum bo'lsa — spinner, sahifa almashsa — tepada progress chizig'i. Bo'sh holat "nega bo'sh" va "endi nima qilish" ni aytadi, xato holati esa sababni oddiy tilda aytib, «Qayta urinish» ni beradi. Hammasi ekran o'quvchiga ham eshitilsin:
role="status",role="alert",aria-busy.
Bu darsda
- Har ma'lumotli ekranning to'rt holatini loyihalaysiz va ularni bitta union turida saqlaysiz.
- Kechiktirilgan ko'rsatkich yozasiz: tez javobda u umuman chiqmaydi, sekin javobda miltillamaydi.
- Skeleton, spinner va progress chizig'idan qaysi biri qachon kerakligini aniqlaysiz.
- Bo'sh holatning ikki turini (birinchi marta va "topilmadi") farqlaysiz.
- Xato holatiga «Qayta urinish» qo'shasiz: TanStack Query va error boundary bilan.
vazifalarga ikonkali bo'sh holat va 300 ms kechikadigan skeleton qo'shasiz — Tailwind'ninghiddentuzog'i bilan birga.
Oldin bilishingiz kerak: Suspense: kutish holatini deklarativ ko'rsatish, Error boundary, TanStack Query I: useQuery asoslari, Ikonkalar, Command palette va klaviatura yorliqlari.
1. Nega bu kerak?
O'tgan darsda buyruqlar oynasi kodini kech yukladik. Birinchi Ctrl+K da oyna ochiladi, ichida bir lahza «Yuklanmoqda…» turadi. Kutish holatini o'ylab qo'yganimiz uchun Bobur bo'sh oynaga qarab qolmadi.
Endi «Bahor» admin panelini Jasur aka telefonida ochdi va uchta shikoyat qildi:
- "Buyurtmalar sahifasi ochilganda bir soniya hech narsa yo'q. Ishlamayapti deb o'yladim."
- "Ba'zan aylanuvchi belgi chaqmoqdek yiltillab o'chadi. Ko'zim charchaydi."
- "Qidiruvga «lag'mon» deb yozdim — bo'sh oq joy. Topilmadimi yoki internet yo'qmi?"
Uchalasida ham kod to'g'ri ishlaydi. Ma'lumot keladi, xato yo'q. Muammo — ma'lumot yo'q paytdagi ekranda. Dasturchilar odatda "hammasi yaxshi" holatni chizadi, qolganini unutadi. Foydalanuvchi esa aynan qolgan holatlarda ilovaga ishonchini yo'qotadi.
Hayotiy o'xshatish: taksi ilovasi. Mashina qidirilayotganda xaritada to'lqin tarqaladi (yuklanish). Mashina topilmasa: "Yaqin atrofda bo'sh mashina yo'q. 5 daqiqadan keyin urinib ko'ring" (bo'sh holat va maslahat). Internet uzilsa: "Aloqa yo'q. Qayta urinish" (xato). Uchala ekran ham kimdir o'ylab chizgani uchun bor.
2. To'rt holat
2.1 Holatlar xaritasi
Serverdan ro'yxat oladigan har komponent shu holatlardan birida turadi:
stateDiagram-v2
state "Bo'sh" as Bosh
[*] --> Yuklanmoqda
Yuklanmoqda --> Tayyor: ma'lumot keldi
Yuklanmoqda --> Bosh: ro'yxat bo'sh
Yuklanmoqda --> Xato: so'rov yiqildi
Xato --> Yuklanmoqda: Qayta urinish
Bosh --> Yuklanmoqda: filtr o'zgardi
Tayyor --> Yuklanmoqda: yangilashNimaga qarang: «Bo'sh» — xato emas, alohida holat. «Xato» dan esa chiqish yo'li bor — qayta urinish. Strelkasiz holat — foydalanuvchi uchun boshi berk ko'cha.
Bu to'rtta asosiy holat. Real ilovada yana ikkitasi uchraydi: eskirgan (eski ma'lumot ko'rinib turibdi, yangisi yo'lda — useDeferredValue darsidagi xira ro'yxat) va qisman (ro'yxat keldi, lekin rasmlar hali yuklanmoqda).
2.2 Bitta union — imkonsiz holat yo'q
Ko'p boshlovchilar holatni uchta alohida useState da saqlaydi: loading, error, data. Unda loading: true va error: "..." bir paytda bo'lib qolishi mumkin. Ekran nima ko'rsatishini bilmaydi.
Discriminated union bilan holat bitta bo'ladi va tur maydoni qaysi holatdaligini aytadi. «Bahor» menyusi. Soxta server uch xil javob bera oladi — tugmalar bilan tanlang:
import { useState } from "react";
const MENU = ["Osh — 35 000", "Manti — 30 000"];
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
async function fetchMenu(scenario) {
await wait(200);
if (scenario === "error") throw new Error("Server javob bermadi");
return scenario === "empty" ? [] : MENU;
}
function MenuView({ state }) {
switch (state.type) {
case "idle":
return <p>Variantni tanlang.</p>;
case "loading":
return <p role="status">Menyu yuklanmoqda…</p>;
case "error":
return <p role="alert">Menyuni olib bo'lmadi.</p>;
case "ready":
return state.dishes.length === 0 ? (
<p>Bugun menyu hali tuzilmagan.</p>
) : (
<ul>
{state.dishes.map((d) => (
<li key={d}>{d}</li>
))}
</ul>
);
}
}
export default function App() {
const [state, setState] = useState({ type: "idle" });
async function load(scenario) {
setState({ type: "loading" });
try {
const dishes = await fetchMenu(scenario);
setState({ type: "ready", dishes });
} catch {
setState({ type: "error" });
}
}
return (
<div>
<button onClick={() => load("ok")}>To'liq</button>{" "}
<button onClick={() => load("empty")}>Bo'sh</button>{" "}
<button onClick={() => load("error")}>Xato</button>
<MenuView state={state} />
</div>
);
}Ekranda:
To'liq Bo'sh Xato
Menyuni olib bo'lmadi.«Ishga tushir» dan keyin uchala tugmani bosib ko'ring. Har safar 200 ms davomida «Menyu yuklanmoqda…» chiqadi, keyin uch xil natijadan biri.
switch dagi har case — bitta holat. Yangi holat qo'shsangiz va uni chizishni unutsangiz, TypeScript loyihasida never tekshiruvi buni ushlaydi (discriminated union darsidagidek).
Rollarga e'tibor bering. role="status" — Malika (ekran o'quvchi) uchun "muloyim" e'lon: u gapini tugatgach o'qiladi. role="alert" — darhol, gapni bo'lib o'qiladi. Yuklanish — status, xato — alert. Bu ikkisi jonli hudud (live region) ning tayyor rollari.
2.3 TanStack Query'da xuddi shu to'rtlik
TanStack Query bu holatlarni o'zi saqlaydi — union'ni qo'lda yozish shart emas:
| Holat | Query'da |
|---|---|
| Yuklanmoqda (birinchi marta) | isPending |
| Xato | isError, error |
| Bo'sh | isSuccess va data.length === 0 |
| Tayyor | isSuccess |
| Eskirgan (fonda yangilanmoqda) | isFetching && !isPending |
Oxirgi qator muhim. Ma'lumot bor, lekin Query fonda yangisini so'rayapti. Bu paytda butun ro'yxatni skeleton bilan almashtirish — xato: foydalanuvchi o'qiyotgan narsani yo'qotadi. To'g'risi — kichik belgi ("Yangilanmoqda…") yoki hech narsa.
Tekshirib ko'ring: Kod
if (isPending) return <Skeleton />deb yozilgan. Ro'yxat ochiq, foydalanuvchi oynaga qaytdi va Query fonda yangilay boshladi (refetchOnWindowFocus). Skeleton chiqadimi?
Javob
Yo'q. isPending faqat hali ma'lumot yo'q bo'lganda rost. Fonda yangilashda ma'lumot bor, shuning uchun isPending — false, isFetching esa — true. Agar kod isFetching ga qarab skeleton ko'rsatganda edi, har oynaga qaytishda ro'yxat bir lahza yo'qolardi.
3. Yuklanish: qachon va qanday ko'rsatish
3.1 Vaqt chegaralari
Foydalanuvchi kutishni qanday his qiladi? Jakob Nielsen'ning mashhur uch chegarasi (1993, "Response Times: The 3 Important Limits"):
| Kutish | Foydalanuvchi his qiladi | Nima ko'rsatish |
|---|---|---|
| 0.1 s gacha | "darhol" | hech narsa |
| 0.1–1 s | sezadi, lekin fikri uzilmaydi | ko'pincha hech narsa yoki kechiktirilgan ko'rsatkich |
| 1–10 s | kutyapti, bezovta | ko'rsatkich (skeleton, spinner) |
| 10 s dan ko'p | boshqa ishga o'tadi | progress foizi, bekor qilish |
Jadvaldan muhim xulosa: tez javobda ko'rsatkich zarar. 150 ms da kelgan javob uchun spinner chiqib-o'chsa, foydalanuvchi faqat miltillashni ko'radi. Jasur akaning ikkinchi shikoyati aynan shu.
3.2 Kechiktirilgan ko'rsatkich
Kechiktirilgan ko'rsatkich (delayed spinner) — yuklanish boshlangach darhol emas, ma'lum vaqtdan keyin chiqadigan belgi. Ikki qoidasi bor:
- Kechikish — 200–300 ms. Javob shu vaqtgacha kelsa, ko'rsatkich umuman chiqmaydi.
- Eng kam ko'rinish — chiqqan bo'lsa, kamida ~500 ms tursin. Aks holda 310 ms da kelgan javob ko'rsatkichni 10 ms ga "yiltillatadi".
Bu g'oyani spin-delay kutubxonasi (2.0.1) hook qilib beradi. Lekin u 30 qatorlik ish — o'zimiz yozamiz. Soxta server ikki tezlikda javob beradi:
import { useEffect, useRef, useState } from "react";
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
// active — yuklanyaptimi; delay — qancha kutib chiqadi;
// minVisible — chiqqan bo'lsa, eng kamida qancha turadi
function useDelayedFlag(active, delay = 300, minVisible = 500) {
const [shown, setShown] = useState(false);
const shownAt = useRef(0);
useEffect(() => {
if (active && !shown) {
const t = setTimeout(() => {
shownAt.current = Date.now();
setShown(true);
}, delay);
return () => clearTimeout(t);
}
if (!active && shown) {
const left = minVisible - (Date.now() - shownAt.current);
const t = setTimeout(() => setShown(false), Math.max(0, left));
return () => clearTimeout(t);
}
}, [active, shown, delay, minVisible]);
return shown;
}
export default function App() {
const [loading, setLoading] = useState(false);
const [result, setResult] = useState("—");
const spinner = useDelayedFlag(loading);
const timesShown = useRef(0);
useEffect(() => {
if (spinner) {
timesShown.current += 1;
console.log("ko'rsatkich chiqdi:", timesShown.current);
}
}, [spinner]);
async function load(ms) {
setLoading(true);
await wait(ms);
setResult(`${ms} ms da keldi`);
setLoading(false);
}
return (
<div>
<button onClick={() => load(100)}>Tez (100 ms)</button>{" "}
<button onClick={() => load(900)}>Sekin (900 ms)</button>
{spinner && <p role="status">Yuklanmoqda…</p>}
<p>Natija: {result}</p>
</div>
);
}Ekranda:
Tez (100 ms) Sekin (900 ms)
Natija: 100 ms da keldiKonsol bo'sh — ko'rsatkich umuman chiqmadi. Javob 100 ms da keldi, 300 ms lik taymer bekor bo'ldi (effekt cleanup'i). Endi sekin so'rov:
Konsolda:
ko'rsatkich chiqdi: 1Ekranda:
Tez (100 ms) Sekin (900 ms)
Natija: 900 ms da keldiTugmani bosib, kuzating: 500 ms atrofida ko'rsatkich hali turibdi. 900 ms da javob keladi va ko'rsatkich yo'qoladi.
Vaqt chizig'ida:
| Vaqt | Tez (100 ms) | Sekin (900 ms) |
|---|---|---|
| 0 ms | loading = true, taymer 300 |
loading = true, taymer 300 |
| 100 ms | javob, taymer bekor — hech narsa ko'rinmadi | — |
| 300 ms | — | ko'rsatkich chiqdi |
| 900 ms | — | javob; 600 ms turdi (≥ 500), darhol yo'qoladi |
Tekshirib ko'ring: Server 320 ms da javob berdi. Ko'rsatkich qachon chiqadi va qachon yo'qoladi?
Javob
300 ms da chiqadi. 320 ms da javob keladi, lekin ko'rsatkich atigi 20 ms turgan. minVisible - 20 = 480 ms kutiladi va u 800 ms da yo'qoladi. Natija matni esa 320 ms da yangilanadi. Ya'ni ko'rsatkich chiqqan bo'lsa, kamida yarim soniya ko'rinadi — yiltillash yo'q. Narxi: shu 480 ms davomida natija tayyor bo'lsa ham ko'rsatkich turadi. Shuning uchun minVisible ni katta qilmang.
3.3 Kechikish — CSS bilan, JavaScript'siz
Kechikishni CSS ham qila oladi: element doim chiziladi, lekin animatsiya uni dastlabki 300 ms ko'rinmas (visibility: hidden) ushlab turadi. vazifalar aynan shu usulni tanladi. Pastdagi natija haqiqiy brauzerda ishlaydi — ikki tugmani bosib, skeleton qachon chiqishini kuzating:
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
.card { min-height: 6.5rem; padding: 0.75rem;
border: 1px solid #ccd; border-radius: 0.5rem; }
/* 300 ms ko'rinmaydi, keyin birdan chiqadi */
.skeleton { animation: appear 0s 300ms both; }
@keyframes appear { from { visibility: hidden; } }
.line { height: 1rem; margin: 0.5rem 0;
border-radius: 0.25rem; background: #d5d9e2; }
.line:last-child { width: 60%; }
@media (prefers-reduced-motion: no-preference) {
.line { animation: pulse 1.5s ease-in-out infinite; }
}
@keyframes pulse { 50% { opacity: 0.45; } }
</style>
<p>
<button id="fast">Tez javob (150 ms)</button>
<button id="slow">Sekin javob (1500 ms)</button>
</p>
<div class="card">
<div class="skeleton" id="skeleton" hidden>
<p>Buyurtmalar yuklanmoqda…</p>
<div class="line"></div>
<div class="line"></div>
<div class="line"></div>
</div>
<ul id="list" hidden>
<li>№101 — Osh, 2 porsiya</li>
<li>№102 — Manti, 5 dona</li>
</ul>
</div>
<script>
const skeleton = document.getElementById("skeleton");
const list = document.getElementById("list");
function load(ms) {
list.hidden = true;
skeleton.hidden = false; // animatsiya qaytadan boshlanadi
setTimeout(() => {
const seen = getComputedStyle(skeleton).visibility;
console.log(`${ms} ms: skeleton — ${seen}`);
skeleton.hidden = true;
list.hidden = false;
}, ms);
}
document.getElementById("fast").onclick = () => load(150);
document.getElementById("slow").onclick = () => load(1500);
</script>«Tez javob» ni bosing — ro'yxat darhol chiqadi, konsolda 150 ms: skeleton — hidden. Skeleton DOM'da edi, lekin ko'rinmadi. «Sekin javob» da esa 300 ms dan keyin kulrang chiziqlar pulsatsiya qiladi va konsolda 1500 ms: skeleton — visible.
Ikki detalga qarang:
animation: appear 0s 300ms both— davomiylik 0, faqat kechikish (300ms).both— kechikish paytida birinchi kadr (visibility: hidden) qo'llanadi. Animatsiya "yugurmaydi", shuning uchun harakatni kamaytirish so'ralganda ham to'g'ri ishlaydi.- Pulsatsiya faqat
prefers-reduced-motion: no-preferenceda. Harakatdan boshi aylanadigan foydalanuvchi oddiy kulrang chiziqlarni ko'radi.
Element hidden dan chiqqanda animatsiya qaytadan boshlanadi — har yangi yuklash yana 300 ms kutadi. JavaScript usulidan farqi: eng kam ko'rinish vaqti yo'q. Lekin taymer ham, state ham, qayta render ham yo'q.
3.4 Skeleton, spinner yoki progress chizig'i?
Suspense darsida skeleton yasaganmiz — kelajakdagi kontent shaklidagi kulrang bo'laklar. Uchta ko'rsatkichni solishtiramiz:
| Ko'rsatkich | Qachon | Afzalligi |
|---|---|---|
| Skeleton | kontent shakli ma'lum (ro'yxat, karta, jadval) | sahifa sakramaydi (CLS 0), nima kelishi ko'rinadi |
| Spinner | shakli noma'lum yoki kichik joy (tugma ichida) | sodda, joy egallamaydi |
| Progress chizig'i | butun sahifa almashadi (navigatsiya) | eski sahifa qoladi, tepada ingichka chiziq |
| Foizli progress | uzoq va o'lchanadigan ish (fayl yuklash) | qancha qolganini aytadi |
Spinner (aylanuvchi belgi) — aylanib turadigan kichik doira. Tugma ichidagi spinner'ning to'g'ri yozilishi — ikonka bezak, ma'no esa matnda:
import { useState } from "react";
import { Loader2 } from "lucide-react";
const css = `
.spin { animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
.spin { animation-duration: 3s; }
}
`;
export default function App() {
const [saving, setSaving] = useState(false);
return (
<div>
<style>{css}</style>
<button
onClick={() => setSaving(true)}
disabled={saving}
aria-busy={saving}
>
{saving && <Loader2 size={16} className="spin" />}
{saving ? "Saqlanmoqda…" : "Saqlash"}
</button>
</div>
);
}Ekranda:
Saqlanmoqda…Loader2 ikonkasi lucide'dan — Ikonkalar darsidagi qoida bo'yicha u o'zi aria-hidden="true" qo'yadi. Malika "Saqlanmoqda…, tugma, o'chirilgan" ni eshitadi — aylanayotgan rasmni emas. Harakatni kamaytirish so'ralganda spinner to'xtamaydi, sekinlashadi: to'liq to'xtagan spinner "ilova qotib qoldi" degan taassurot beradi.
Maslahat: Faqat ikonkadan iborat spinner (
<Loader2 />va hech qanday matn yo'q) — Malika uchun jim. Matn ko'rinmasin desangiz, uni "faqat ekran o'quvchi uchun" sinf bilan yashiring yoki ota elementgaaria-labelbering.
3.5 Progress chizig'i: navigatsiya paytida
Sahifadan sahifaga o'tishda butun ekranni skeleton bilan almashtirish shart emas. Eski sahifa qolsin, tepada ingichka chiziq yursin — YouTube va GitHub shunday qiladi. Bu progress chizig'i (top progress bar).
Tayyor kutubxonalar: nprogress (0.2.0, oxirgi versiyasi 2015-yilda chiqqan, lekin hali ko'p loyihada uchraydi) va Next.js uchun nextjs-toploader (3.9.17). React Router'da kutubxona shart emas: loader ishlayotganini useNavigation() aytadi. Natija oynasida manzil satri yo'q, shuning uchun xotiradagi router (19-qismdagidek):
import {
createMemoryRouter,
RouterProvider,
Link,
Outlet,
useNavigation,
useLoaderData,
} from "react-router";
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const css = `
.bar { position: fixed; inset: 0 0 auto; height: 3px;
background: #1d4ed8; transform-origin: left;
animation: grow 2s ease-out both; }
@keyframes grow { from { transform: scaleX0.1-bob; }
to { transform: scaleX0.9-bob; } }
.sr { position: absolute; width: 1px; height: 1px;
overflow: hidden; clip-path: inset(50%); }
`;
function Layout() {
const navigation = useNavigation();
return (
<div>
<style>{css}</style>
{navigation.state === "loading" && (
<div className="bar" role="status">
<span className="sr">Sahifa yuklanmoqda…</span>
</div>
)}
<nav>
<Link to="/">Bosh sahifa</Link> <Link to="/menyu">Menyu</Link>
</nav>
<Outlet />
</div>
);
}
function Menu() {
const dishes = useLoaderData();
return <p>Menyu: {dishes.join(", ")}</p>;
}
const router = createMemoryRouter([
{
path: "/",
element: <Layout />,
children: [
{ index: true, element: <p>Xush kelibsiz!</p> },
{
path: "menyu",
loader: async () => {
await wait(400);
return ["Osh", "Manti"];
},
element: <Menu />,
},
],
},
]);
export default function App() {
return <RouterProvider router={router} />;
}Ekranda:
Bosh sahifa Menyu
Menyu: Osh, Manti«Menyu» ni bosganda 400 ms davomida «Xush kelibsiz!» joyida qoladi, tepada ko'k chiziq o'sadi. Shu oraliqda chiziq ichida «Sahifa yuklanmoqda» degan yashirin matn bor — u Malika (ekran o'quvchi) uchun. Chiziq hech qachon 100% ga yetmaydi — server qachon javob berishini bilmaymiz. U shunchaki "ish ketyapti" deydi.
4. Bo'sh holat
4.1 Ikki xil "bo'sh"
Bo'sh holat (empty state) — ro'yxatda ko'rsatadigan narsa yo'q paytdagi ekran. Lekin "bo'sh" ning sababi har xil:
| Tur | Misol | Nima deyish | Harakat |
|---|---|---|---|
| Birinchi marta | yangi foydalanuvchi, vazifa yo'q | "Hali vazifa yo'q" | «Birinchisini qo'shing» |
| Filtr natijasi | qidiruvda «lag'mon» | "«lag'mon» bo'yicha topilmadi" | «Filtrni tozalash» |
| Hammasi bajarildi | bugungi buyurtmalar tugadi | "Bugungi hammasi tayyor" | (tabrik, harakat shart emas) |
Jasur akaning uchinchi shikoyati — bo'sh oq joy. U nima bo'lganini bilmadi. Yaxshi bo'sh holat uch narsani aytadi: nima bo'sh, nega va endi nima qilish.
import { useState } from "react";
import { Search } from "lucide-react";
const DISHES = ["Osh", "Manti", "Somsa"];
function EmptyResult({ query, onClear }) {
return (
<div className="empty">
<Search size={32} />
<p>«{query}» bo'yicha taom topilmadi.</p>
<button onClick={onClear}>Filtrni tozalash</button>
</div>
);
}
export default function App() {
const [query, setQuery] = useState("");
const found = DISHES.filter((d) =>
d.toLowerCase().includes(query.trim().toLowerCase()),
);
return (
<div>
<input
aria-label="Qidiruv"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
{found.length === 0 ? (
<EmptyResult query={query} onClear={() => setQuery("")} />
) : (
<ul>
{found.map((d) => (
<li key={d}>{d}</li>
))}
</ul>
)}
</div>
);
}Ekranda:
Osh
Manti
SomsaQidiruvga «lag'mon» deb yozganimizda ekranda «lag'mon» bo'yicha taom topilmadi.» va tugma bor edi. «Filtrni tozalash» uni qaytardi. «Ishga tushir» da o'zingiz ham yozib ko'ring.
Ikonka (Search) — bezak: lucide uni aria-hidden qiladi, ma'no matnda. Bo'sh holatdagi tugma esa eng muhim qism — u foydalanuvchini boshi berk ko'chadan olib chiqadi.
Diqqat: "Topilmadi" xabari dinamik paydo bo'ladi. Malika qidiruv maydonida yozayotganda uni ko'rmaydi. Natijalar soni yoki "topilmadi" ni
role="status"li alohida joyda e'lon qiling —vazifalarda bu 18-qismdan beri bor (#royxat-holati).
4.2 Tuzoq: hidden va display: flex
Bo'sh holatni markazga qo'yish uchun odatda flex ishlatiladi. vazifalar da bo'sh xabar hidden atributi bilan yashiriladi. Shu ikkisi birga kelganda qiziq xato chiqadi — haqiqiy brauzerda ko'ring:
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
.flex { display: flex; gap: 0.5rem; align-items: center; }
.ok:not([hidden]) { display: flex; gap: 0.5rem; }
p { padding: 0.5rem; border: 1px dashed #99a; }
</style>
<p class="flex" hidden>❌ hidden + .flex — yashirin bo'lishi kerak!</p>
<p class="ok" hidden>✅ Bu ko'rinmaydi</p>
<p class="ok">✅ hidden yo'q — flex ishlaydi</p>Birinchi qator ko'rinib turibdi, garchi hidden bo'lsa ham! Sabab — kaskad (CSS kaskadi). hidden atributini brauzerning o'z stil jadvali display: none ga aylantiradi. Sizning .flex { display: flex } qoidangiz esa muallif stili — u brauzer stilidan doim ustun. Tailwind'ning flex sinfi ham aynan shunday ishlaydi.
Davosi — flex'ni faqat hidden bo'lmaganda berish: :not([hidden]). Tailwind'da bu ixtiyoriy variant: [&:not([hidden])]:flex. vazifalar qadamida aynan shuni ko'rasiz.
Tekshirib ko'ring:
hiddeno'rniga React'da shartli chizish ({found.length === 0 && <Empty />}) ishlatsak, bu tuzoq bormi?
Javob
Yo'q. Shartli chizishda element DOM'da umuman bo'lmaydi — yashiradigan narsa yo'q. Tuzoq faqat element DOM'da qolib, atribut bilan yashirilganda chiqadi. vazifalar da #bosh-xabar doim DOM'da turadi: unga e2e testlar va aria-describedby havolalari tayanadi. Shuning uchun u yerda hidden va :not([hidden]) kerak.
5. Xato holati va qayta urinish
5.1 Yaxshi xato xabari
Xato xabari ham UI. Uch qoida:
- Oddiy til. "Failed to fetch" yoki
TypeErroremas — "Buyurtmalarni olib bo'lmadi". Texnik tafsilot konsolda va xato kuzatuv tizimida qolsin. - Sabab, agar ma'lum bo'lsa. "Internet aloqasi yo'q" va "Server javob bermadi" — foydalanuvchi uchun har xil: birinchisida u Wi-Fi'ni tekshiradi.
- Chiqish yo'li. «Qayta urinish» tugmasi. Xato qisman bo'lsa — faqat shu qism xato ko'rsatsin, butun sahifa emas (Error boundary darsidagi chegara joylashuvi).
5.2 Query bilan qayta urinish
Query xatoni o'zi bir necha marta qayta urinadi (standart — 3 marta, oraliq ortib boradi). Bu tarmoq bir lahza uzilganda yordam beradi. Lekin hammasi yiqilsa, qaror foydalanuvchida: unga refetch ni bering. Soxta server birinchi marta yiqiladi, ikkinchisida javob beradi:
import {
QueryClient,
QueryClientProvider,
useQuery,
} from "@tanstack/react-query";
const queryClient = new QueryClient();
let calls = 0;
function fetchOrders() {
calls += 1;
const attempt = calls;
return new Promise((resolve, reject) =>
setTimeout(() => {
console.log("so'rov", attempt);
if (attempt === 1) reject(new Error("503"));
else resolve(["№101 — Osh", "№102 — Manti"]);
}, 200),
);
}
function Orders() {
const q = useQuery({
queryKey: ["orders"],
queryFn: fetchOrders,
retry: false, // darsda: avtomatik urinishsiz
});
if (q.isPending) return <p role="status">Yuklanmoqda…</p>;
if (q.isError) {
return (
<div role="alert">
<p>Buyurtmalarni olib bo'lmadi. Server javob bermadi.</p>
<button onClick={() => q.refetch()} disabled={q.isFetching}>
{q.isFetching ? "Yuklanmoqda…" : "Qayta urinish"}
</button>
</div>
);
}
return (
<ul>
{q.data.map((o) => (
<li key={o}>{o}</li>
))}
</ul>
);
}
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Orders />
</QueryClientProvider>
);
}Konsolda:
so'rov 1
so'rov 2Ekranda:
№101 — Osh
№102 — MantiUch nuqtaga e'tibor bering:
- Qayta urinish paytida xato xabari joyida qoladi, faqat tugma «Yuklanmoqda…» bo'ladi va o'chadi.
isPendingbu paytdafalse— ma'lumot hali yo'q, lekin Query xato holatidan qayta so'rayapti, shuning uchunisFetchingqaraymiz. disabled={q.isFetching}— sabrsiz bosish beshta so'rov yubormaydi.retry: falsefaqat darsda: Query'ning uch avtomatik urinishi 7 soniyaga cho'zilardi. Loyihada standartni qoldiring yokiretry: 1qiling.
5.3 Render xatosi: error boundary bilan
Xato ba'zan so'rovda emas, chizishda bo'ladi — masalan, kod yoki ma'lumot shakli kutilmagan. Uni error boundary ushlaydi. vazifalar ning statistika panelida xuddi shunday: XatoChegarasi ichida «Qayta urinish» tugmasi bor (holat 12 dan parcha):
<XatoChegarasi
tiklashKaliti={kechikkan}
fallback={(_xato, qaytaUrin) => (
<p id="batafsil-xato" role="alert">
Statistikani yuklab yoki hisoblab bo'lmadi.{" "}
<Tugma
onClick={() => (yuklandi ? qaytaUrin() : location.reload())}
>
Qayta urinish
</Tugma>
</p>
)}
>Asl kodda onClick ichida izohli if bor, bu yerda qisqartirilgan. Ikki xil tiklash: kod (chunk) kelgan bo'lsa — chegarani tiklash kifoya; chunk kelmagan bo'lsa — lazy xatoni eslab qolgan, faqat sahifani yangilash yordam beradi (lazy va code splitting).
6. Suspense bilan: skeleton qayerga qo'yiladi
Suspense bilan kutish holati komponent ichida emas, chegarada yoziladi: fallback={<Skeleton />}. Ikki eslatma (18-qismdan, qisqa):
- Transition bilan boshlangan yangilanish allaqachon ko'rinib turgan kontentni fallback'ga almashtirmaydi — eski ekran qoladi. Bu "eskirgan" holatning React'dagi tayyor yo'li.
- React fallback'ni ko'rsatgach, uni ~300 ms ushlab turadi — xuddi bizning
minVisibleg'oyasi, miltillamasligi uchun.
Lekin bitta narsani React qilmaydi: fallback'ni kechiktirib ko'rsatish. Chegara birinchi marta suspend bo'lganda fallback darhol chiziladi. Shuning uchun vazifalar skeleton'ga CSS kechikishini qo'shdi.
7. Vazifalar qadami: bo'sh holat va kechikadigan skeleton (holat 12)
7.1 Nima o'zgaradi
Ikki kichik o'zgarish, ikkalasi ham bugungi g'oyalar:
#bosh-xabar— ikonka (ListTodo) va markazdagi matn. Matnlar o'zgarmaydi: "Hozircha vazifa yo'q. Birinchisini qo'shing!" (birinchi marta) va "«…» bo'yicha vazifa topilmadi." (filtr natijasi).- Statistika panelining Suspense fallback'i — matn o'rniga skeleton: 300 ms kechikib chiqadi, pulsatsiya faqat
motion-safeda.
git switch -c feat/bosh-holatlarvazifalar.css dagi @theme ga yangi animatsiya tokeni:
/* 20/#12: yuklanish ko'rsatkichi 300 ms kechikib chiqadi
(animate-kechikib): tez javobda miltillamaydi. Davomiylik 0 —
faqat kechikish, shuning uchun prefers-reduced-motion'da ham
to'g'ri ishlaydi */
--animate-kechikib: kechikib-chiqish 0s 300ms both;
@keyframes kechikib-chiqish {
from { visibility: hidden; }
}Tailwind v4 da --animate-* nomlar fazosidagi token animate-kechikib sinfini yaratadi, @keyframes esa shu @theme ichida yoziladi (Tailwind CSS 1: tokenlar). Bu natija misolidagi appear animatsiyasining o'zi.
App.tsx — bo'sh holat (kanondan):
{/* 20/#12: bo'sh holat — ikonka (bezak) + matn markazda.
Tailwind'ning "flex" sinfi hidden atributini BOSIB KETADI
(utilitalar brauzer uslubidan ustun) — shuning uchun
faqat [hidden] bo'lmaganda */}
<p
id="bosh-xabar"
hidden={korinadigan.length > 0}
className="flex-col items-center gap-2 py-6 text-center text-ikkinchi [&:not([hidden])]:flex"
>
<ListTodo size={32} />
{normalla(sorov) === ""
? boshMatnlar[filtr]
: `«${sorov.trim()}» bo'yicha vazifa topilmadi.`}
</p>className qatori 70 belgidan uzun — kanon kodi o'zgartirilmadi (izohlar telefon uchun qatorlarga bo'lingan). Undagi [&:not([hidden])]:flex — «hidden va display: flex» bo'limidagi davo. flex-col va items-center esa hidden ga ta'sir qilmaydi: ular display ni o'zgartirmaydi.
StatistikaPaneli.tsx — fallback (kanondan):
<Suspense
fallback={
// 20/#12: skeleton — kelajakdagi jadval shaklida
// kulrang bloklar (sahifa siljimaydi).
// animate-kechikib: 300 ms ko'rinmaydi — tez
// yuklansa miltillamaydi (kechiktirilgan yuklanish
// ko'rsatkichi); motion-safe: — harakatni
// kamaytirish so'ralgan bo'lsa, pulsatsiya yo'q
<div className="animate-kechikib">
<p id="batafsil-kutish">Statistika tayyorlanmoqda…</p>
<div aria-hidden="true" className="grid gap-2">
{[0, 1, 2].map((i) => (
<div
key={i}
className="h-6 rounded-burchak bg-chegara motion-safe:animate-pulse"
/>
))}
</div>
</div>
}
>Uch nuqta:
animate-kechikibbutun fallback'da — matn ham, chiziqlar ham 300 ms ko'rinmaydi. Statistika odatda shu vaqtdan tez hisoblanadi: foydalanuvchi hech qanday kutishni ko'rmaydi.aria-hidden="true"— kulrang chiziqlar Malika uchun ma'nosiz. U «Statistika tayyorlanmoqda…» matnini eshitadi.motion-safe:animate-pulse—motion-safevarianti@media (prefers-reduced-motion: no-preference)ga aylanadi. Bundan tashqariasos.cssda 18-qismdan beri umumiy qoida bor: harakatni kamaytirish so'ralganda har animatsiya davomiyligi 0.01 ms. Bizningkechikibanimatsiyasi esa baribir ishlaydi: unda davomiylik emas, kechikish (animation-delay) bor, umumiy qoida esa kechikishga tegmaydi.
7.2 Tekshiruvlar
git add -A
git commitCommit (kanon): feat: bo'sh holat (ikonka), skeleton — 300 ms kechikib, motion-safe. Diff: 3 fayl, +33 −2.
Tekshiruvlar (haqiqiy, holat 12):
npm test— node:testpass 198, VitestTests 65 passed (65);lint,format:check,tip,knip— toza. axe — 7 ko'rinishda 0 buzilish.- Build: birinchi yuklash JS o'zgarmadi (ikonka allaqachon to'plamda — lucide'dan bitta nom), CSS 4.27 KB gzip.
- Brauzer (Chrome 155): 159, 72, 100 va 20-qism ssenariysiga to'rtta yangi tekshiruv:
✅ ro'yxat bor — bo'sh holat yashirin (hidden → display: none, flex bosmaydi): [true,"none"]
✅ topilmadi — markazda, ikonka bezak (aria-hidden), matn o'sha: [false,"flex","true","«zzz» bo'yicha vazifa topilmadi."]
✅ kechiktirilgan ko'rsatkich: @keyframes va motion-safe pulsatsiya CSS'da: [true,true]
✅ bo'sh holat: CSP buzilishi yo'q: []Birinchi qatordagi "none" — haqiqiy Chrome'da hisoblangan display: Tailwind'ning flex i hidden ni bosib ketmadi. Ikkinchisida "flex" — qidiruv natijasi bo'sh, xabar markazda, ikonkada aria-hidden="true". Oxirgisi — CSS animatsiya va visibility CSP bilan muammosiz: hammasi .css faylda, hech qanday <style> tegi qo'shilmadi.
8. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| spinner yiltillab o'chadi | ko'rsatkich darhol chiqadi | 200–300 ms kechikish, chiqsa ≥ 500 ms |
| fonda yangilashda ro'yxat yo'qoladi | isFetching ga qarab skeleton |
skeleton faqat isPending da |
loading va error bir paytda rost |
uchta alohida useState |
bitta union (type maydoni) |
| bo'sh oq joy, foydalanuvchi adashadi | bo'sh holat chizilmagan | nima, nega, endi nima qilish |
| "topilmadi" bilan "xato" bir xil matn | bo'sh va xato holati aralashgan | alohida holatlar, alohida matn |
hidden bo'lsa ham element ko'rinadi |
.flex / Tailwind flex brauzer stilini bosdi |
:not([hidden]) / [&:not([hidden])]:flex |
| sahifa yuklanganda sakraydi | spinner kichik, kontent katta | kontent shaklidagi skeleton |
| ekran o'quvchi jim | faqat ikonka yoki kulrang bloklar | role="status" matn, bloklarga aria-hidden |
| «Qayta urinish» besh so'rov yubordi | tugma kutish paytida faol | disabled={isFetching} |
9. Mashqlar
Mashqlar kurs/mashqlar/20/12-yuklanish-holatlari/ da. Har yechimni «Ishga tushir» bilan sinang.
1-mashq (oson): qaysi ko'rsatkich?
Vaziyatga mos ko'rsatkichni yozing — skeleton, spinner, progress (tepadagi chiziq) yoki hech (hech narsa):
- Taomlar kartochkalari ro'yxati, javob odatda 1–2 soniyada keladi →
- «Saqlash» tugmasi bosildi, natija qancha kutishi noma'lum →
- «Menyu» havolasidan «Buyurtmalar» sahifasiga o'tish →
- Qidiruv natijasi har doim 50 ms ichida keladi →
Yechim
- skeleton — kontent shakli ma'lum (kartochkalar), kutish sezilarli: sahifa sakramaydi.
- spinner — kichik joy (tugma ichida), shakli yo'q. Matn bilan: «Saqlanmoqda…».
- progress — navigatsiya: eski sahifa qoladi, tepada chiziq (
useNavigation). - hech — 0.1 s dan tez javob "darhol" seziladi. Har qanday ko'rsatkich faqat miltillaydi.
2-mashq (o'rta): bo'sh va xato aralashib ketgan
«Bahor» buyurtmalar paneli. Soxta server ikki xil javob beradi: «Bo'sh kun» — buyurtma yo'q, «Uzilish» — xato. Hozirgi kod ikkalasida ham bir xil "Buyurtma yo'q" deydi va qayta urinish yo'q. Tuzating:
- Bo'sh: «Bugun hali buyurtma yo'q.»
- Xato:
role="alert"ichida «Buyurtmalarni olib bo'lmadi.» va «Qayta urinish» tugmasi — u oxirgi so'rovni takrorlaydi.
Ishora: holatni bitta union qiling — «Bitta union — imkonsiz holat yo'q» bo'limidagidek (type: idle, loading, error, ready). Oxirgi stsenariyni eslab qolish uchun yana bitta useState.
import { useState } from "react";
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
async function fetchOrders(scenario) {
await wait(200);
if (scenario === "down") throw new Error("503");
return [];
}
export default function App() {
const [orders, setOrders] = useState(null);
async function load(scenario) {
try {
setOrders(await fetchOrders(scenario));
} catch {
setOrders([]);
}
}
return (
<div>
<button onClick={() => load("empty")}>Bo'sh kun</button>{" "}
<button onClick={() => load("down")}>Uzilish</button>
{orders !== null && orders.length === 0 && <p>Buyurtma yo'q</p>}
</div>
);
}Ekranda:
Bo'sh kun Uzilish
Buyurtma yo'qYechim
import { useState } from "react";
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
async function fetchOrders(scenario) {
await wait(200);
if (scenario === "down") throw new Error("503");
return [];
}
export default function App() {
const [state, setState] = useState({ type: "idle" });
const [last, setLast] = useState(null);
async function load(scenario) {
setLast(scenario);
setState({ type: "loading" });
try {
const orders = await fetchOrders(scenario);
setState({ type: "ready", orders });
} catch {
setState({ type: "error" });
}
}
return (
<div>
<button onClick={() => load("empty")}>Bo'sh kun</button>{" "}
<button onClick={() => load("down")}>Uzilish</button>
{state.type === "loading" && (
<p role="status">Yuklanmoqda…</p>
)}
{state.type === "error" && (
<div role="alert">
<p>Buyurtmalarni olib bo'lmadi.</p>
<button onClick={() => load(last)}>Qayta urinish</button>
</div>
)}
{state.type === "ready" && state.orders.length === 0 && (
<p>Bugun hali buyurtma yo'q.</p>
)}
</div>
);
}Ekranda:
Bo'sh kun Uzilish
Buyurtmalarni olib bo'lmadi.
Qayta urinish«Qayta urinish» oxirgi stsenariyni (last) takrorlaydi — bizning soxta serverda u yana «Uzilish», shuning uchun xato qaytdi. Haqiqiy serverda ikkinchi urinish ko'pincha o'tadi. Muhimi — endi foydalanuvchi "bo'sh" va "buzilgan" ni farqlaydi. Asl koddagi catch { setOrders([]) } xatoni "yutib" yuborgan edi: xato bo'sh ro'yxatga aylangan.
3-mashq (qiyin): ko'rsatkich + skeleton + eng kam vaqt
«Bahor» buyurtmalar ro'yxati. Talablar:
- «Yuklash» tugmasi — soxta server 700 ms da ikki buyurtma qaytaradi.
- Kutish paytida skeleton (uchta
<div className="line" />) — lekin 300 ms kechikib chiqsin. Skeleton ichidarole="status"matn «Buyurtmalar yuklanmoqda…», chiziqlararia-hidden. - Skeleton chiqqan bo'lsa, kamida 500 ms tursin.
- Konsolga uch voqeani yuz bergan tartibda yozing:
skeleton chiqdi,javob keldi,skeleton yo'qoldi.
Ishora: «Kechiktirilgan ko'rsatkich» bo'limidagi useDelayedFlag ni oling. Skeleton voqealarini useEffect da (skeleton o'zgarganda), javobni — await dan keyin yozing. Millisekundlarni konsolga chiqarmang: taymerlar har kompyuterda bir necha ms farq qiladi, tartib esa o'zgarmaydi.
Yechim
import { useEffect, useRef, useState } from "react";
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
function useDelayedFlag(active, delay = 300, minVisible = 500) {
const [shown, setShown] = useState(false);
const shownAt = useRef(0);
useEffect(() => {
if (active && !shown) {
const t = setTimeout(() => {
shownAt.current = Date.now();
setShown(true);
}, delay);
return () => clearTimeout(t);
}
if (!active && shown) {
const left = minVisible - (Date.now() - shownAt.current);
const t = setTimeout(() => setShown(false), Math.max(0, left));
return () => clearTimeout(t);
}
}, [active, shown, delay, minVisible]);
return shown;
}
export default function App() {
const [orders, setOrders] = useState(null);
const [loading, setLoading] = useState(false);
const skeleton = useDelayedFlag(loading);
const wasShown = useRef(false);
useEffect(() => {
if (skeleton) {
wasShown.current = true;
console.log("skeleton chiqdi");
} else if (wasShown.current) {
console.log("skeleton yo'qoldi");
}
}, [skeleton]);
async function load() {
setLoading(true);
await wait(700);
console.log("javob keldi");
setOrders(["№101 — Osh", "№102 — Manti"]);
setLoading(false);
}
return (
<div>
<button onClick={load}>Yuklash</button>
{skeleton ? (
<div>
<p role="status">Buyurtmalar yuklanmoqda…</p>
<div aria-hidden="true">
<div className="line" />
<div className="line" />
<div className="line" />
</div>
</div>
) : (
orders && (
<ul>
{orders.map((o) => (
<li key={o}>{o}</li>
))}
</ul>
)
)}
</div>
);
}Konsolda:
skeleton chiqdi
javob keldi
skeleton yo'qoldiEkranda:
Yuklash
№101 — Osh
№102 — MantiTartibga qarang: javob kelgandan keyin ham skeleton bir muddat turdi. U ~300 ms da chiqqan, javob ~700 ms da keldi — skeleton atigi 400 ms ko'ringan edi. Eng kam ko'rinish 500 ms, shuning uchun u yana ~100 ms turib, ~800 ms da yo'qoldi. Ro'yxat ham shu paytda chiqadi — skeleton ko'rinib turganda orders ni chizmaymiz. Bu ataylab: skeleton va kontent bir paytda ko'rinmasin. wasShown — birinchi chizishdagi false ni e'tiborsiz qoldiradi (hali hech narsa ko'rsatilmagan).
4-mashq: Vazifalar qadami
feat/bosh-holatlar branch'ida uch faylni o'zgartiring: vazifalar.css (--animate-kechikib va @keyframes), App.tsx (#bosh-xabar — ListTodo va [&:not([hidden])]:flex), StatistikaPaneli.tsx (skeleton fallback). Keyin commit.
Yechim
Tekshiruv ro'yxati:
- Vazifa bor — DevTools'da
#bosh-xabaruchun Computeddisplay: none. Qidiruvga «zzz» — xabar markazda, ikonka bilan. - DevTools → Rendering → "Emulate CSS media feature prefers-reduced-motion: reduce" — skeleton chiziqlari pulsatsiya qilmaydi.
- Network'da "Slow 4G" — statistika panelini oching: matn va chiziqlar 300 ms dan keyin chiqadi. Tezkor tarmoqda — umuman ko'rinmaydi.
npm test— node 198, Vitest 65;npm run tip— 0 xato. Konsolda CSP xatosi yo'q.
10. Real ishda
- Dizayn tizimlarida
Skeleton,Spinner,EmptyState— alohida komponentlar (shadcn'daSkeletonvaEmpty). Dizayner har ekran uchun to'rt holatni chizadi. Chizmagan bo'lsa — so'rang, bu sizning ishingizni yengillashtiradi. - Mahsulot sifati. Bo'sh holat — yangi foydalanuvchining birinchi ekrani. U yerda "Birinchisini qo'shing" tugmasi bo'lmasa, odam ilovani yopadi. Ko'p jamoalar bo'sh holatni onboarding qismi deb ko'radi.
- Kuzatuv. Xato holati chiqqanda xato kuzatuv tizimiga (masalan, Sentry) yozuv tushadi — foydalanuvchi qo'ng'iroq qilishidan oldin jamoa biladi.
- Intervyu savollari: "Skeleton va spinner farqi?", "Spinner miltillashini qanday oldini olasiz?", "Bo'sh va xato holatini qanday ajratasiz?", "Ekran o'quvchi yuklanishni qanday biladi?", "
isPendingvaisFetchingfarqi?".
Xulosa
- Ma'lumotli ekran — to'rt holat: yuklanmoqda, xato, bo'sh, tayyor (+ eskirgan, qisman). Holatni bitta union'da saqlang.
- Tez javobga ko'rsatkich kerak emas: 200–300 ms kechikish, chiqsa — kamida ~500 ms. CSS'da:
animation: … 0s 300ms bothvavisibility: hidden. - Shakli ma'lum kontent — skeleton, noma'lum — spinner, navigatsiya — progress chizig'i (
useNavigation). - Bo'sh holat: nima, nega, endi nima qilish. "Birinchi marta" va "topilmadi" — har xil matn va harakat.
- Xato: oddiy til, sabab, «Qayta urinish» (
refetch, error boundary'ni tiklash), tugma kutishda o'chiq. hidden+display: flex— flex yutadi;:not([hidden]). Ekran o'quvchi uchunrole="status",role="alert", bloklargaaria-hidden.
Keyingi dars: Karusel va slayderlar — CSS scroll-snap bilan JavaScript'siz karusel, Embla (shadcn standarti) va Swiper, avtomatik aylanish va a11y; nega vazifalar da karusel yo'q.
Manbalar
- Nielsen Norman Group: "Response Times: The 3 Important Limits" — nngroup.com/articles/response-times-3-important-limits
- TanStack Query: Queries, Query Retries — tanstack.com/query/latest/docs/framework/react/guides/queries (5.104.1)
- React Router:
useNavigation— reactrouter.com (8.4.0) - MDN:
animation-delay,animation-fill-mode,prefers-reduced-motion, ARIAstatusvaalertrollari — developer.mozilla.org - Tailwind CSS v4: Theme variables (
--animate-*),motion-safe— tailwindcss.com/docs (4.3.3)
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!