IlmHamroh
JavaScript Full-stack/20-qism. React: UI kutubxonalar va animatsiya12/30-dars28 daqiqa
Mundarija (32)

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.
  • vazifalar ga ikonkali bo'sh holat va 300 ms kechikadigan skeleton qo'shasiz — Tailwind'ning hidden tuzog'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:

  1. "Buyurtmalar sahifasi ochilganda bir soniya hech narsa yo'q. Ishlamayapti deb o'yladim."
  2. "Ba'zan aylanuvchi belgi chaqmoqdek yiltillab o'chadi. Ko'zim charchaydi."
  3. "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: yangilash

Nimaga 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:

jsx
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:

text
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:

  1. Kechikish — 200–300 ms. Javob shu vaqtgacha kelsa, ko'rsatkich umuman chiqmaydi.
  2. 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:

jsx
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:

text
Tez (100 ms) Sekin (900 ms)
Natija: 100 ms da keldi

Konsol 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:

jsx

Konsolda:

text
ko'rsatkich chiqdi: 1

Ekranda:

text
Tez (100 ms) Sekin (900 ms)
Natija: 900 ms da keldi

Tugmani 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:

html
<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-preference da. 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:

jsx
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:

text
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 elementga aria-label bering.

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):

jsx
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:

text
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.

jsx
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:

text
Osh
Manti
Somsa

Qidiruvga «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 — vazifalar da 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:

html
<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: hidden o'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:

  1. Oddiy til. "Failed to fetch" yoki TypeError emas — "Buyurtmalarni olib bo'lmadi". Texnik tafsilot konsolda va xato kuzatuv tizimida qolsin.
  2. Sabab, agar ma'lum bo'lsa. "Internet aloqasi yo'q" va "Server javob bermadi" — foydalanuvchi uchun har xil: birinchisida u Wi-Fi'ni tekshiradi.
  3. 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:

jsx
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:

text
so'rov 1
so'rov 2

Ekranda:

text
№101 — Osh
№102 — Manti

Uch nuqtaga e'tibor bering:

  • Qayta urinish paytida xato xabari joyida qoladi, faqat tugma «Yuklanmoqda…» bo'ladi va o'chadi. isPending bu paytda false — ma'lumot hali yo'q, lekin Query xato holatidan qayta so'rayapti, shuning uchun isFetching qaraymiz.
  • disabled={q.isFetching} — sabrsiz bosish beshta so'rov yubormaydi.
  • retry: false faqat darsda: Query'ning uch avtomatik urinishi 7 soniyaga cho'zilardi. Loyihada standartni qoldiring yoki retry: 1 qiling.

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):

tsx
<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 minVisible g'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:

  1. #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).
  2. Statistika panelining Suspense fallback'i — matn o'rniga skeleton: 300 ms kechikib chiqadi, pulsatsiya faqat motion-safe da.
bash
git switch -c feat/bosh-holatlar

vazifalar.css dagi @theme ga yangi animatsiya tokeni:

css
  /* 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):

tsx
      {/* 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):

tsx
        <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-kechikib butun 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-safe varianti @media (prefers-reduced-motion: no-preference) ga aylanadi. Bundan tashqari asos.css da 18-qismdan beri umumiy qoida bor: harakatni kamaytirish so'ralganda har animatsiya davomiyligi 0.01 ms. Bizning kechikib animatsiyasi esa baribir ishlaydi: unda davomiylik emas, kechikish (animation-delay) bor, umumiy qoida esa kechikishga tegmaydi.

7.2 Tekshiruvlar

bash
git add -A
git commit

Commit (kanon): feat: bo'sh holat (ikonka), skeleton — 300 ms kechikib, motion-safe. Diff: 3 fayl, +33 −2.

Tekshiruvlar (haqiqiy, holat 12):

  • npm test — node:test pass 198, Vitest Tests 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:
text
✅ 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:

  1. Bo'sh: «Bugun hali buyurtma yo'q.»
  2. 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.

jsx
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:

text
Bo'sh kun Uzilish
Buyurtma yo'q
Yechim
jsx
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:

text
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:

  1. «Yuklash» tugmasi — soxta server 700 ms da ikki buyurtma qaytaradi.
  2. Kutish paytida skeleton (uchta <div className="line" />) — lekin 300 ms kechikib chiqsin. Skeleton ichida role="status" matn «Buyurtmalar yuklanmoqda…», chiziqlar aria-hidden.
  3. Skeleton chiqqan bo'lsa, kamida 500 ms tursin.
  4. 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
jsx
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:

text
skeleton chiqdi
javob keldi
skeleton yo'qoldi

Ekranda:

text
Yuklash
№101 — Osh
№102 — Manti

Tartibga 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-xabar uchun Computed display: 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'da Skeleton va Empty). 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?", "isPending va isFetching farqi?".

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 both va visibility: 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 uchun role="status", role="alert", bloklarga aria-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, ARIA status va alert rollari — developer.mozilla.org
  • Tailwind CSS v4: Theme variables (--animate-*), motion-safe — tailwindcss.com/docs (4.3.3)
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Yuklanish, bo'sh va xato holatlari UX React'da: skeleton, kechiktirilgan spinner va qayta urinish — IlmHamroh