IlmHamroh
JavaScript Full-stack/19-qism. React: routing, server state, formalar5/36-dars30 daqiqa
Mundarija (40)

React Router data mode: loader, action, Form, useFetcher va errorElement

Qisqacha: Data mode'da har marshrutga loader (ma'lumotni olish) va action (o'zgartirish) funksiyasi biriktiriladi. Router sahifani chizishdan oldin loader'ni chaqiradi, komponent natijani useLoaderData() bilan oladi — useEffect va "yuklanmoqda" state'i kerak emas. <Form method="post"> yuborilganda router action ni chaqiradi, keyin sahifadagi loader'larni qayta ishga tushiradi. Xato tashlansa (throw data("…", { status: 404 })), sahifa o'rnida eng yaqin errorElement chiziladi.

Bu darsda

  • loader yozasiz va useLoaderData bilan sahifaga ma'lumot berasiz; nega bu useEffect dagi yuklashdan tezroq ekanini tushuntirasiz.
  • useNavigation bilan butun ilova uchun "yuklanmoqda" belgisini, hydrateFallbackElement bilan birinchi ochilishdagi holatni ko'rsatasiz.
  • <Form> va action bilan formani yuborasiz, useActionData bilan xatoni qaytarasiz, redirect bilan boshqa sahifaga o'tasiz.
  • useFetcher bilan sahifadan ketmasdan amal bajarasiz ("Yoqdi" tugmasi).
  • errorElement, useRouteError va isRouteErrorResponse bilan 404 va kutilmagan xatoni chiroyli ko'rsatasiz; vazifalar dagi vazifa sahifasini serverga ulaysiz.

Oldin bilishingiz kerak: React Router III: dasturiy navigatsiya va URL holati, Effektda ma'lumot olish, Actions va form action, Error boundary.

1. Nega bu kerak?

«Bahor» saytida taom sahifasi bor: /taom/osh. Hozircha taomlar kodning ichida yozilgan. Jasur aka endi narxlarni serverdan olmoqchi — oshxonada narx o'zgarsa, sayt darhol bilsin.

Sardor buni effekt darsidagidek yozdi: komponent chiziladi, useEffect so'rov yuboradi, javob kelguncha "Yuklanmoqda…". Ishlaydi. Lekin uchta muammo chiqdi:

  1. Kech boshlanadi. So'rov komponent chizilgandan keyin ketadi. Undan oldin router sahifani tanladi, React chizdi — vaqt o'tdi.
  2. Zinapoya (waterfall). Taom sahifasi ichida sharhlar bo'limi bor. U ham o'z useEffect ida so'raydi — lekin faqat ota chizilgandan keyin. So'rovlar ketma-ket, parallel emas.
  3. Har sahifada bir xil kod. loading, error, data uch state'i, bekor qilish, poyga holati (race) — har sahifada qaytadan.

Router esa manzilni bosilgan zahoti biladi: /taom/osh — demak taom sahifasi, unga osh kerak. Nega so'rovni darhol yubormaslik kerak? Data mode aynan shuni qiladi.

Hayotiy o'xshatish — oshxonadagi ofitsiant. Yomon ofitsiant mehmonni stolga o'tqazadi, keyin oshxonaga borib buyurtma beradi, keyin choynakka ketadi. Yaxshisi eshikdanoq buyurtmani oshpazga aytadi va choyni ham shu payt qo'yadi. Mehmon o'tirganda hammasi tayyor bo'ladi. loader — eshikdagi buyurtma.

2. loader — ma'lumot sahifadan oldin

2.1 Birinchi loader

Loader — marshrutga biriktirilgan funksiya. Router shu marshrutga o'tishdan oldin uni chaqiradi va qaytgan qiymatni (Promise bo'lsa — natijasini) kutadi. Komponent esa qiymatni useLoaderData() bilan oladi.

Natija oynasida tarmoq yo'q, shuning uchun "server" — 250 ms kechikadigan Promise. Loyihada uning o'rnida fetch turadi.

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

// Soxta server: 250 ms dan keyin javob
const wait = (ms, value) =>
  new Promise((resolve) => setTimeout(() => resolve(value), ms));

function Shell() {
  const navigation = useNavigation();
  const busy = navigation.state === "loading";
  return (
    <div>
      <p>{busy ? "Yuklanmoqda…" : "Tayyor"}</p>
      <Outlet />
    </div>
  );
}

function Dish() {
  const dish = useLoaderData();
  console.log("chizildi:", dish.name);
  return (
    <p>
      {dish.name} — {dish.price} so'm
    </p>
  );
}

const router = createMemoryRouter([
  {
    path: "/",
    element: <Shell />,
    children: [
      { index: true, element: <Link to="/taom/osh">Osh</Link> },
      {
        path: "taom/:slug",
        loader: ({ params }) => {
          console.log("loader:", params.slug);
          return wait(250, { name: "Osh", price: 35000 });
        },
        element: <Dish />,
      },
    ],
  },
]);

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

Konsolda:

text
loader: osh
chizildi: Osh

Ekranda:

text
Tayyor
Osh — 35000 so'm

«Osh» bosildi — va sahifa darhol almashmadi. Router 250 ms davomida eski sahifada qoldi, yuqorida "Yuklanmoqda…" turdi (harakat qatoridagi ekran("Yuklanmoqda…") shuni tekshiradi). Javob kelgach, Dish birinchi chizilishidayoq tayyor ma'lumot bilan chizildi. Konsolga qarang: chizildi faqat bir marta, va u loader'dan keyin.

Komponentda useState yo'q, useEffect yo'q, if (loading) yo'q. dish hech qachon undefined emas — router uni kafolatlaydi.

Loader'ga router bitta obyekt beradi:

Maydon Nima Misol
params dinamik segmentlar params.slug — "osh"
request Fetch API'ning Request obyekti request.url, request.signal

request.url dan so'rov parametrlari olinadi: new URL(request.url).searchParams.get("turi"). request.signal haqida — «Bekor qilish» bo'limida.

2.2 Ikki yo'l yonma-yon

Effekt darsida tartib shunday edi: chizish ("Yuklanmoqda…") → so'rov → yana chizish. Loader bilan: so'rov → chizish, bitta chizish. Ikki sahifa ichma-ich bo'lsa, farq ikki barobar bo'ladi:

sequenceDiagram
  participant R as Router
  participant S as Server
  Note over R,S: useEffect bilan
  R->>R: taom sahifasi chizildi
  R->>S: taom?
  S-->>R: osh
  R->>R: sharhlar chizildi
  R->>S: sharhlar?
  S-->>R: 3 ta sharh
  Note over R,S: loader bilan
  R->>S: taom? va sharhlar? (bir vaqtda)
  S-->>R: osh, 3 ta sharh
  R->>R: hammasi chizildi

Yuqorida — zinapoya: har qavat o'z navbatini kutadi. Pastda — ikkala so'rov birga ketdi.

2.3 Ichma-ich loader'lar parallel

Router mos kelgan barcha marshrutlarni oldindan biladi (/ → taom/:slug → sharhlar). Shuning uchun ularning loader'larini bir vaqtda chaqiradi:

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

const wait = (ms, value) =>
  new Promise((resolve) => setTimeout(() => resolve(value), ms));

function log(text) {
  console.log(text);
}

function Dish() {
  const dish = useLoaderData();
  return (
    <section>
      <h2>{dish}</h2>
      <Outlet />
    </section>
  );
}

function Reviews() {
  const reviews = useLoaderData();
  return <p>Sharhlar: {reviews.join(", ")}</p>;
}

const router = createMemoryRouter([
  {
    path: "/",
    element: <Link to="/taom/osh/sharhlar">Osh sharhlari</Link>,
  },
  {
    path: "/taom/:slug",
    element: <Dish />,
    loader: async () => {
      log("taom: boshlandi");
      const dish = await wait(200, "Osh");
      log("taom: tugadi");
      return dish;
    },
    children: [
      {
        path: "sharhlar",
        element: <Reviews />,
        loader: async () => {
          log("sharhlar: boshlandi");
          const list = await wait(300, ["mazali", "issiq"]);
          log("sharhlar: tugadi");
          return list;
        },
      },
    ],
  },
]);

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

Konsolda:

text
taom: boshlandi
sharhlar: boshlandi
taom: tugadi
sharhlar: tugadi

Ekranda:

text
Osh
Sharhlar: mazali, issiq

Ikkala "boshlandi" birinchi chiqdi — so'rovlar parallel. Umumiy kutish 200 + 300 emas, eng uzuni — 300 ms. Har komponent useLoaderData() bilan o'z marshrutining ma'lumotini oladi: Dish — taomni, Reviews — sharhlarni.

Maslahat: Bola komponentga ota marshrutning ma'lumoti kerak bo'lsa — useRouteLoaderData("taom"). Buning uchun ota marshrutga id: "taom" beriladi. Ikkinchi yo'l — oldingi darsdagi <Outlet context>.

Tekshirib ko'ring: Taom loader'i 200 ms, sharhlar loader'i 300 ms. useEffect bilan (sharhlar ota chizilgandan keyin so'raladi) va loader bilan sahifa qancha vaqtda to'liq chiziladi?

Javob

useEffect bilan — taxminan 500 ms: avval taom (200), u chizilgach sharhlar so'raladi (yana 300). Loader bilan — 300 ms: ikkalasi bir vaqtda boshlanadi, router eng sekinini kutadi. Sahifa qancha ko'p qavatli bo'lsa, farq shuncha katta.

3. Kutish paytida nima ko'rinadi?

3.1 useNavigation — butun ilova uchun bitta holat

Loader ishlayotganda router eski sahifani ekranda ushlab turadi. Foydalanuvchiga "bosdim, nimadir bo'lyapti" degan belgi kerak. useNavigation() hozirgi o'tishning holatini beradi:

navigation.state Qachon
"idle" hech narsa kutilmayapti
"loading" yangi sahifaning loader'lari ishlayapti
"submitting" forma yuborildi, action ishlayapti

Birinchi misoldagi Shell shuni ishlatdi. Bu hook istalgan komponentda ishlaydi — odatda layout'da: sahifa tepasidagi ingichka chiziq, xira qilingan kontent. navigation.location — qayerga ketilyapti.

Eski sahifa ekranda turishi — ataylab. Transition darsidagi g'oyaning o'zi: tayyor bo'lmagan sahifa o'rniga bo'sh ekran ko'rsatgandan ko'ra, eskisini ushlab turish yaxshiroq.

3.2 Birinchi ochilish: hydrateFallbackElement

Do'st havolani Telegram'da oldi va /taom/osh ni to'g'ridan ochdi. Endi "eski sahifa" yo'q — ilova shu zahotida boshlandi. Loader 250 ms ishlaydi. Shu paytda ekranda nima bo'ladi?

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

const wait = (ms, value) =>
  new Promise((resolve) => setTimeout(() => resolve(value), ms));

function Dish() {
  const dish = useLoaderData();
  return <p>{dish.name}</p>;
}

const router = createMemoryRouter(
  [
    {
      path: "/taom/:slug",
      loader: () => wait(250, { name: "Osh" }),
      element: <Dish />,
    },
  ],
  { initialEntries: ["/taom/osh"] },
);

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

Konsolda:

text
No `HydrateFallback` element provided to render during initial hydration

Ekranda:

text
Osh

Sahifa chizildi, lekin 250 ms davomida ekran bo'sh edi va konsolda ogohlantirish chiqdi. Tarjimasi: "Dastlabki yuklanish paytida chiziladigan HydrateFallback elementi berilmagan". Davosi — marshrutga hydrateFallbackElement:

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

const wait = (ms, value) =>
  new Promise((resolve) => setTimeout(() => resolve(value), ms));

function Dish() {
  const dish = useLoaderData();
  return <p>{dish.name}</p>;
}

const router = createMemoryRouter(
  [
    {
      path: "/taom/:slug",
      loader: () => wait(250, { name: "Osh" }),
      hydrateFallbackElement: <p role="status">Yuklanmoqda…</p>,
      element: <Dish />,
    },
  ],
  { initialEntries: ["/taom/osh"] },
);

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

Ekranda:

text
Osh

Avval "Yuklanmoqda…" (ekran qadami tekshirdi), keyin osh. Ogohlantirish yo'q. role="status" — Malika (ekran o'quvchi) ham holatni eshitadi.

Diqqat: Birinchi ochilishda loader har doim sahifani kechiktiradi. Ro'yxat kabi asosiy sahifada bu sezilarli: foydalanuvchi bo'sh ekranga qaraydi, keyin kontent "sakraydi" (CLS — Core Web Vitals). Shuning uchun vazifalar da ro'yxat sahifasiga loader qo'yilmadi — u brauzerdagi nusxadan darhol chiziladi. Loader faqat vazifa sahifasida.

4. action va <Form> — o'zgartirish

4.1 Forma router'ga yuboriladi

Ma'lumot olish — yarmi. Mehmon sharh ham yozadi. HTML formasini eslang (Forma qanday ishlaydi): <form method="post"> yuborilsa, brauzer ma'lumotni serverga jo'natib, yangi sahifani yuklardi. React Router'ning <Form> komponenti xuddi shunday yoziladi, lekin yuborishni brauzer emas, router ushlaydi. Router ma'lumotni marshrutning action funksiyasiga beradi. Sahifa qayta yuklanmaydi.

jsx
import {
  createMemoryRouter,
  Form,
  RouterProvider,
  useLoaderData,
  useNavigation,
} from "react-router";

const wait = (ms, value) =>
  new Promise((resolve) => setTimeout(() => resolve(value), ms));

// "Server" xotirasi
const reviews = ["Issiq keldi"];

function Reviews() {
  const list = useLoaderData();
  const busy = useNavigation().state === "submitting";
  return (
    <section>
      <ul>
        {list.map((text) => (
          <li key={text}>{text}</li>
        ))}
      </ul>
      <Form method="post">
        <input name="text" aria-label="Sharh" />
        <button disabled={busy}>
          {busy ? "Yuborilmoqda…" : "Yuborish"}
        </button>
      </Form>
    </section>
  );
}

const router = createMemoryRouter([
  {
    path: "/",
    element: <Reviews />,
    loader: () => {
      console.log("loader");
      return [...reviews];
    },
    action: async ({ request }) => {
      const form = await request.formData();
      console.log("action:", form.get("text"));
      await wait(250);
      reviews.push(form.get("text"));
      return null;
    },
  },
]);

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

Konsolda:

text
loader
action: Juda mazali
loader

Ekranda:

text
Issiq keldi
Juda mazali
Yuborish

Konsoldagi uch qatorni ketma-ket o'qing:

  1. loader — sahifa ochildi.
  2. action: Juda mazali — forma yuborildi. request.formData() — inputlarning name va qiymatlari, oddiy HTML formadagidek (FormData).
  3. loader — yana. Action tugagach router sahifadagi loader'larni o'zi qayta chaqirdi. Bu — revalidatsiya (revalidation): "ma'lumot o'zgargan bo'lishi mumkin, qaytadan olaman".

setState yo'q: server o'zgardi, router yangisini oldi. Shu paytda useNavigation().state avval "submitting", keyin "loading" (revalidatsiya), oxirida "idle" bo'ldi.

Bu Actions darsidagi <form action={fn}> ga o'xshaydi. Farqi: React'ning form action'i — komponent ichidagi funksiya. Router action'i — marshrutga bog'langan va tugagach ma'lumotni o'zi yangilaydi.

4.2 Bitta action — bir nechta amal: niyat

Bir sahifada ikki forma bo'lishi mumkin: "Belgilash" va "O'chirish". Action esa bitta. Ularni ajratish uchun yashirin maydon qo'yiladi:

jsx
<Form method="post">
  <input type="hidden" name="niyat" value="ochir" />
  <button>O'chirish</button>
</Form>

Action ichida: const niyat = form.get("niyat") va if (niyat === "ochir") …. Ingliz tilidagi hujjatlarda bu naqsh intent deyiladi. vazifalar da aynan shunday qilinadi.

4.3 Xato qaytarish: useActionData va status

Bo'sh sharh yuborilsa-chi? Action xatoni qaytaradi (tashlamaydi), komponent uni useActionData() bilan o'qiydi:

jsx
import {
  createMemoryRouter,
  data,
  Form,
  RouterProvider,
  useActionData,
  useLoaderData,
} from "react-router";

function Reviews() {
  const list = useLoaderData();
  const result = useActionData();
  return (
    <Form method="post">
      <p>Sharhlar: {list.length} ta</p>
      <input name="text" aria-label="Sharh" />
      <button>Yuborish</button>
      {result?.error && <p role="alert">{result.error}</p>}
    </Form>
  );
}

const router = createMemoryRouter([
  {
    path: "/",
    element: <Reviews />,
    loader: () => {
      console.log("loader");
      return ["Issiq keldi"];
    },
    action: async ({ request }) => {
      const text = String((await request.formData()).get("text"));
      console.log("action:", JSON.stringify(text));
      if (text.trim() === "") {
        const error = "Sharh bo'sh bo'lmasin";
        return data({ error }, { status: 400 });
      }
      return null;
    },
  },
]);

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

Konsolda:

text
loader
action: ""

Ekranda:

text
Sharhlar: 1 ta
Yuborish
Sharh bo'sh bo'lmasin

Ikki narsaga qarang. Birinchisi — useActionData() action qaytargan obyektni berdi, xato ekranga chiqdi. Ikkinchisi — konsolda loader ikkinchi marta chaqirilmadi. data(…, { status: 400 }) — "javob, lekin xato holati bilan". 4xx yoki 5xx status bilan qaytgan action'dan keyin router revalidatsiya qilmaydi: hech narsa o'zgarmadi, qayta so'rash — ortiqcha. Status'siz oddiy return { error } qilsangiz, loader baribir qayta ishlaydi.

Maslahat: Bu tekshiruv — foydalanuvchiga qulaylik. Haqiqiy serverda ham xuddi shu tekshiruv bo'lishi shart: brauzerdagi kodni har kim chetlab o'tishi mumkin. Forma tekshiruvining to'liq vositalari — React Hook Form va Zod darslarida.

4.4 Amaldan keyin boshqa sahifa: redirect

Buyurtma berilgach — tasdiq sahifasiga. Action ichida hook yo'q (useNavigate ishlamaydi — u komponent emas). O'rniga redirect("/manzil") qaytariladi:

jsx
import {
  createMemoryRouter,
  Form,
  redirect,
  RouterProvider,
  useParams,
} from "react-router";

let lastId = 11;

function Order() {
  const { id } = useParams();
  return <p>{id}-buyurtma qabul qilindi!</p>;
}

const router = createMemoryRouter([
  {
    path: "/",
    element: (
      <Form method="post">
        <button>Buyurtma berish</button>
      </Form>
    ),
    action: () => {
      lastId += 1; // serverga saqlash o'rnida
      return redirect(`/buyurtma/${lastId}`);
    },
  },
  { path: "/buyurtma/:id", element: <Order /> },
]);

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

Ekranda:

text
12-buyurtma qabul qilindi!

Oldingi darsdagi navigate("/buyurtma/12") misolining data mode varianti. redirect tarixga yangi yozuv qo'shadi (push). Joriy yozuvni almashtirish kerak bo'lsa — replace("/manzil"): bu navigate(…, { replace: true }) ning loader/action'dagi ko'rinishi. Loader ham redirect yoki replace qaytarishi (yoki tashlashi) mumkin — masalan, eski manzildan yangisiga.

Tekshirib ko'ring: Action return { ok: true } qaytardi. Sahifadagi loader qayta chaqiriladimi? return data({ ok: false }, { status: 500 }) bo'lsa-chi?

Javob

Birinchisida — ha: muvaffaqiyatli action'dan keyin router hamma faol loader'larni qayta chaqiradi (revalidatsiya). Ikkinchisida — yo'q: 4xx/5xx status "hech narsa o'zgarmadi" degan ma'noni beradi. Ikkala holatda ham useActionData() qaytgan obyektni beradi.

5. useFetcher — sahifadan ketmasdan

5.1 Navigatsiya bo'lmasligi kerak bo'lganda

Menyuda har taom yonida «Yoqdi » tugmasi. Bosilsa — serverga yoziladi. Lekin <Form> navigatsiya: u action'ga yuborib, keyin o'sha marshrutga o'tadi, tarixga yozuv qo'shishi va useNavigation ni butun ilovada "submitting" qilishi mumkin. Yoqtirish uchun bu ortiqcha — mehmon menyuda qolishi kerak.

useFetcher() — navigatsiyasiz loader/action chaqiruvchi. Har chaqiruv o'zining kichik "kanali" ga ega: o'z state i, o'z data si. Bir sahifada nechta bo'lsa ham, bir-biriga xalaqit bermaydi.

jsx
import {
  createMemoryRouter,
  RouterProvider,
  useFetcher,
  useLoaderData,
} from "react-router";

const wait = (ms, value) =>
  new Promise((resolve) => setTimeout(() => resolve(value), ms));

const likes = { osh: 4, manti: 2 };

function DishRow({ slug, name, count }) {
  const fetcher = useFetcher();
  return (
    <li>
      {name}: {count} ta{" "}
      <fetcher.Form method="post" action={`/taom/${slug}/yoqtir`}>
        <button>
          {fetcher.state === "idle" ? "Yoqdi ❤️" : "…"}
        </button>
      </fetcher.Form>
    </li>
  );
}

function Menu() {
  const data = useLoaderData();
  return (
    <ul>
      <DishRow slug="osh" name="Osh" count={data.osh} />
      <DishRow slug="manti" name="Manti" count={data.manti} />
    </ul>
  );
}

const router = createMemoryRouter([
  {
    path: "/",
    element: <Menu />,
    loader: () => {
      console.log("menyu loader");
      return { ...likes };
    },
  },
  {
    // Faqat action — sahifa (element) yo'q
    path: "/taom/:slug/yoqtir",
    action: async ({ params }) => {
      await wait(250);
      likes[params.slug] += 1;
      console.log("yoqtirildi:", params.slug);
      return null;
    },
  },
]);

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

Konsolda:

text
menyu loader
yoqtirildi: osh
menyu loader

Ekranda:

text
Osh: 5 ta
Yoqdi ❤️
Manti: 2 ta
Yoqdi ❤️

Faqat bosilgan tugma "…" ga aylandi — boshqa qator tegilmadi (fetcher.state shu qatorniki). Manzil / da qoldi. Action tugagach menyu loader'i baribir qayta ishladi va son 5 bo'ldi: fetcher ham revalidatsiyani boshlaydi.

/taom/:slug/yoqtir marshrutida element yo'q — faqat action. Bunday marshrut resurs marshrut deyiladi: unga sahifa sifatida o'tilmaydi, faqat ma'lumot uchun murojaat qilinadi.

5.2 Fetcher imkoniyatlari

Nima Vazifasi
<fetcher.Form> <Form> ning navigatsiyasiz turi
fetcher.submit(formData, { method, action }) koddan yuborish (masalan, onChange da)
fetcher.load("/manzil") boshqa marshrutning loader'ini chaqirish (qidiruv takliflari)
fetcher.state "idle", "submitting", "loading"
fetcher.data action/loader qaytargan qiymat
fetcher.formData yuborilayotgan ma'lumot — optimistik UI uchun

Oxirgisi qiziq. Bosilgan zahoti sonni oshirib ko'rsatish mumkin — server javobini kutmasdan: const shown = fetcher.formData ? count + 1 : count. Bu useOptimistic g'oyasining router'dagi ko'rinishi. Server rad etsa, formData yo'qoladi va son o'z-o'zidan eski holiga qaytadi.

Maslahat: Qachon <Form>, qachon fetcher? Savol bering: "Amaldan keyin foydalanuvchi boshqa sahifada bo'lishi kerakmi yoki manzil o'zgarishi kerakmi?" Ha — <Form> (yangi buyurtma, kirish, tahrirni saqlash). Yo'q — fetcher (yoqtirish, katakni belgilash, ro'yxatdagi qatorni o'chirish).

6. Xatolar: errorElement

6.1 Loader xato tashlasa

/taom/pitsa — bunday taom yo'q. Loader nima qiladi? Bo'sh obyekt qaytarsa, komponent undefined.name da yiqiladi. To'g'ri yo'l — tashlash (throw). Router uni ushlaydi va sahifa o'rniga marshrutning errorElement ini chizadi. Komponent ichida xatoni useRouteError() beradi.

Xato ikki xil bo'ladi va ularni isRouteErrorResponse() ajratadi:

  • javob-xato — siz ataylab tashlagan throw data("Taom topilmadi", { status: 404 }): status, statusText, data maydonlari bor;
  • kutilmagan xato — TypeError, tarmoq uzilishi va hokazo oddiy Error obyektlari.
jsx
import {
  createMemoryRouter,
  data,
  isRouteErrorResponse,
  Link,
  Outlet,
  RouterProvider,
  useLoaderData,
  useRouteError,
} from "react-router";

const DISHES = { osh: "Osh", manti: "Manti" };

function Shell() {
  return (
    <div>
      <p>«Bahor» menyusi</p>
      <Outlet />
    </div>
  );
}

function Dish() {
  return <p>{useLoaderData()}</p>;
}

function DishError() {
  const error = useRouteError();
  if (isRouteErrorResponse(error)) {
    return (
      <p>
        {error.status}: {error.data}
      </p>
    );
  }
  return <p>Kutilmagan xato: {error.message}</p>;
}

const router = createMemoryRouter([
  {
    path: "/",
    element: <Shell />,
    children: [
      { index: true, element: <Link to="/taom/pitsa">Pitsa</Link> },
      {
        path: "taom/:slug",
        element: <Dish />,
        errorElement: <DishError />,
        loader: ({ params }) => {
          const dish = DISHES[params.slug];
          if (dish === undefined) {
            throw data("Bunday taom yo'q", { status: 404 });
          }
          return dish;
        },
      },
    ],
  },
]);

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

Ekranda:

text
«Bahor» menyusi
404: Bunday taom yo'q

Shell joyida qoldi — "«Bahor» menyusi" ko'rinib turibdi. Xato faqat taom/:slug marshrutining o'rnini egalladi. Konsolda hech narsa yo'q: o'zimiz ushlagan xato "kutilgan" holat.

errorElement — Error boundary darsidagi chegara, faqat marshrutga bog'langan. U loader va action xatolarini ham, sahifa komponenti chizilishidagi xatoni ham ushlaydi.

6.2 Xato yuqoriga ko'tariladi

Marshrutda errorElement bo'lmasa, xato otasiga ko'tariladi — eng yaqin errorElement gacha. Yuqoridagi misolda errorElement ni Shell ga ko'chirsak, xato butun Shell ni almashtiradi: menyu sarlavhasi ham yo'qoladi. Qoida: errorElement ni iloji boricha pastga qo'ying, shunda ilovaning ishlab turgan qismi ko'rinib qoladi.

Hech qayerda errorElement bo'lmasa-chi? Router o'zining standart sahifasini chizadi:

jsx
import {
  createMemoryRouter,
  data,
  RouterProvider,
  useLoaderData,
} from "react-router";

function Dish() {
  return <p>{useLoaderData()}</p>;
}

const router = createMemoryRouter(
  [
    {
      path: "/taom/:slug",
      element: <Dish />,
      loader: () => {
        throw data("Server javob bermadi", { status: 500 });
      },
    },
  ],
  { initialEntries: ["/taom/osh"] },
);

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

Konsolda:

text
Error handled by React Router default ErrorBoundary: ErrorResponseImpl {status: 500, statusText: 'Internal Server Error', data: 'Server javob bermadi', error: undefined, internal: false}

Ekranda:

text
Unexpected Application Error!
500 Internal Server Error
💿 Hey developer 👋
You can provide a way better UX than this when your app throws errors by providing your own ErrorBoundary or errorElement prop on your route.

Ekranda — "Kutilmagan ilova xatosi!" va status. Pastdagi " Hey developer " qatorlari — dasturchiga maslahat: "xato bo'lganda o'z ErrorBoundary yoki errorElement ingizni bersangiz, foydalanuvchiga ancha yaxshi tajriba berasiz". Konsoldagi qator ham shu haqda: "xatoni React Router'ning standart ErrorBoundary'si ushladi", keyin xato obyekti (ErrorResponseImpl — data() ning ichki nomi). Maslahat va konsol qatori faqat development build'da. Production'da foydalanuvchi faqat "Unexpected Application Error!" va statusni ko'radi — inglizcha va yordamsiz. Shuning uchun ildiz marshrutda doim o'z errorElement ingiz bo'lsin (vazifalar da u 2-darsdan beri bor — XatoSahifasi).

Tekshirib ko'ring: Loader'da throw new Error("…") o'rniga nega throw data("…", { status: 404 }) yozdik? Ikkalasi ham errorElement ga tushadi-ku.

Javob

data(…, { status }) — "kutilgan" xato: taom yo'q, ruxsat yo'q. errorElement uni isRouteErrorResponse bilan taniydi va holatga mos matn chiqaradi: 404 — "topilmadi", 403 — "ruxsat yo'q". Oddiy Error — "nimadir buzildi", unga boshqacha munosabat kerak (qayta urinish, xabar berish). Ikkalasini bir xil tashlasak, foydalanuvchiga "Bunday taom yo'q" ni "Server buzildi" dan ajratib ko'rsata olmaymiz.

7. Bekor qilish va kod bo'lish

7.1 request.signal

Mehmon osh sahifasiga bosdi, loader so'rov yubordi. Yarim soniyadan keyin fikrini o'zgartirib, manti'ni bosdi. Osh javobi endi kerak emas. Router eskirgan o'tishni o'zi tashlab yuboradi va request.signal ni bekor qiladi. Uni fetch(url, { signal: request.signal }) ga bersangiz, brauzer so'rovning o'zini ham to'xtatadi (AbortController). signal bermasangiz ham ekranda xato bo'lmaydi — router eski javobni e'tiborsiz qoldiradi. Lekin trafik behuda ketadi.

7.2 lazy — marshrut kodi kerak bo'lganda

Marshrutga lazy berilsa, uning komponenti va loader'i alohida faylga ajratiladi va birinchi o'tishda yuklanadi:

jsx
{
  path: "statistika",
  lazy: async () => {
    const m = await import("./sahifalar/StatistikaSahifasi.tsx");
    return { Component: m.StatistikaSahifasi };
  },
}

Natija oynasida dinamik import() ishlamaydi, shuning uchun blok tekshirilmagan. lazy ning g'oyasi — lazy va kodni bo'lish darsidagi React.lazy bilan bir xil, lekin router uni loader bilan parallel yuklaydi.

Kanonda vazifalar ning statistika va sozlamalar sahifalariga lazy sinab ko'rildi (19/#17, natijalar/17-lazy-marshrut.txt). Sahifalar kodi kichik (0.6 va 0.3 KB gzip), Vite esa umumiy modullarni alohida faylga ajratdi: ro'yxat sahifasi 130.5 KB yukladi (lazy'siz ~129.8) va bitta ortiqcha so'rov. Foyda yo'q — kanonga kirmadi. lazy og'ir sahifalar uchun (grafik kutubxonasi, matn muharriri), kichik sahifani ajratish o'zini oqlamaydi.

8. Vazifalar qadami: vazifa sahifasi serverdan (holat 05)

8.1 Nima quramiz

Hozir vazifa sahifasi ro'yxatni brauzerdagi nusxadan (yukla()) o'qiydi. Boshqa qurilmada o'zgargan vazifa bu yerda eski ko'rinadi. Endi:

  1. Loader vazifani serverdan oladi; oflayn yoki xato bo'lsa — brauzerdagi nusxadan.
  2. Sahifada ikki forma: «Bajarildi deb belgilash» va «O'chirish…» — ikkalasi bitta action ga.
  3. Topilmasa — errorElement (VazifaXatosi), menyu joyida qoladi.
  4. /vazifa/02 → /vazifa/2 endi loader'da (replace()).
bash
git switch -c feat/vazifa-loader

8.2 Loader: vazifa-marshruti.ts

Yangi fayl. Asosiy qismi (kanondan aynan, izohlar telefon uchun qayta bo'lingan):

ts
// vazifa-marshruti.ts — "/vazifa/:id" ning ma'lumot qismi (19/#05,
// data mode). loader — sahifa chizilishidan OLDIN ma'lumotni oladi
// (router kutadi), action — <Form method="post"> yuborilganda
// ishlaydi. Ikkalasi ham React'dan tashqarida: hook yo'q, so'rov —
// request.signal bilan (foydalanuvchi boshqa sahifaga o'tsa, router
// so'rovni bekor qiladi)
import { data, redirect, replace } from "react-router";

const keshdagi = (): Vazifalar => yukla() ?? boshlangichVazifalar;

// Haqiqat manbai — server; u javob bermasa (oflayn, xato) yoki bu
// kalitni hali bilmasa (versiya 0) — shu brauzerdagi nusxa. Bekor
// qilingan so'rov — qayta tashlanadi: router uni o'zi kutadi
async function royxatniOl(signal: AbortSignal): Promise<Vazifalar> {
  if (!navigator.onLine) {
    return keshdagi();
  }
  try {
    const javob = serverRoyxatiniTekshir(await api.royxat({ signal }));
    return javob.versiya === 0 ? keshdagi() : javob.vazifalar;
  } catch (xato) {
    if (xato instanceof ApiXatosi && xato.kod === "BEKOR") {
      throw xato;
    }
    console.warn(`Vazifa keshdan: ${xatoMatni(xato)}`);
    return keshdagi();
  }
}

/** "abc", "1.5" — vazifa yo'q (404); "02" — "2" ga
 * yo'naltiriladi */
function raqaminiOl(id: string | undefined): number {
  const raqam = Number(id);
  if (!Number.isInteger(raqam) || raqam < 1) {
    // data() — xato JAVOB (status bilan): errorElement uni
    // isRouteErrorResponse bilan taniydi
    throw data(`«${id ?? ""}» raqamli vazifa yo'q.`, { status: 404 });
  }
  if (id !== String(raqam)) {
    // Bir vazifa — bitta manzil (19/#04 dagi <Navigate replace>
    // o'rni). replace() — redirect() ning tarixga yozuv
    // QO'SHMAYDIGAN turi
    throw replace(`/vazifa/${raqam}`);
  }
  return raqam;
}

export async function vazifaLoader({
  params,
  request,
}: LoaderFunctionArgs): Promise<VazifaMalumoti> {
  const raqam = raqaminiOl(params.id);
  const vazifa = (await royxatniOl(request.signal)).find(
    (v) => v.id === raqam,
  );
  if (vazifa === undefined) {
    throw data(`«${raqam}» raqamli vazifa yo'q.`, { status: 404 });
  }
  return vazifa;
}

Darsdagi g'oyalar shu yerda: params.id tekshiriladi va noto'g'ri bo'lsa throw data(…, { status: 404 }); request.signal API qatlamiga uzatiladi (bekor qilish xatosi "BEKOR" yutilmaydi — router uni o'zi taniydi); server javob bermasa — yiqilmaydi, brauzerdagi nusxa; eski manzil — throw replace(…).

Diqqat: Kanonning birinchi variantida bu yerda redirect() edi. Oldingi darsdagi «Orqaga» tuzatishining yangi e2e tekshiruvi buni topdi: /vazifa/02 ochilganda tarix +1 bo'ldi. «Orqaga» /vazifa/02 ga qaytdi, loader yana /vazifa/2 ga otdi — aylana (natijalar/04-orqaga-tekshiruv.txt: "e2e-19 37 2 "). replace() ga almashtirildi. Yo'naltirish deyarli doim replace bilan — oldingi darsdagi qoida loader'da ham kuchda.

Komponent endi ancha qisqa. VazifaSahifasi.tsx dan useParams, yukla(), "topilmadi" tarmog'i va <Navigate> olib tashlandi. O'rniga bitta qator:

tsx
  // typeof vazifaLoader — tur loader'ning qaytargan qiymatidan
  const vazifa = useLoaderData<typeof vazifaLoader>();

useLoaderData<typeof vazifaLoader>() — TypeScript turni loader'ning qaytish turidan oladi: vazifa — VazifaMalumoti, qo'lda yozish shart emas. Loader o'zgarsa, tur ham o'zgaradi.

8.3 Action: belgilash va o'chirish

Xuddi o'sha faylda (kanondan aynan):

ts
export async function vazifaAction({
  params,
  request,
}: ActionFunctionArgs): Promise<VazifaAmalNatijasi | Response> {
  const raqam = raqaminiOl(params.id);
  const forma = await request.formData();
  const niyat = forma.get("niyat");
  const sozlama = { signal: request.signal };
  try {
    if (niyat === "ochir") {
      await api.ochir(raqam, sozlama);
      keshniYangila((eski) => eski.filter((v) => v.id !== raqam));
      return redirect("/");
    }
    if (niyat === "almashtir") {
      const bajarildi = forma.get("bajarildi") === "true";
      const vazifa = serverVazifasiniTekshir(
        await api.yangila(raqam, { bajarildi }, sozlama),
      );
      keshniYangila((eski) =>
        eski.map((v) => (v.id === raqam ? vazifa : v)),
      );
      return {
        ok: true,
        xabar: bajarildi ? "Bajarildi deb belgilandi." : "Yana faol.",
      };
    }
  } catch (xato) {
    return { ok: false, xabar: `Saqlanmadi: ${xatoMatni(xato)}` };
  }
  throw data("Noma'lum amal", { status: 400 });
}
  • niyat — «Bitta action — bir nechta amal» bo'limidagi naqsh.
  • O'chirilgach — redirect("/"): vazifa yo'q, uning sahifasida qolish ma'nosiz. Bu yerda push to'g'ri — o'chirish yangi qadam.
  • Server rad etsa — tashlamaydi, qaytaradi: { ok: false, xabar }. Sahifa joyida qoladi, xabar ko'rinadi.
  • keshniYangila — brauzerdagi nusxa va boshqa tablar (BroadcastChannel). Ro'yxat sahifasi ochilganda ham to'g'ri holat bo'lsin.

8.4 Sahifa: VazifaUmumiy.tsx da formalar

Index bo'limga ikki <Form> (kanondan, qisqartirilgan):

tsx
  // Action natijasi (oxirgi yuborishniki): xabar yoki xato
  const natija = useActionData<typeof vazifaAction>();
  // Yuborilmoqda: tugma matni — router holatidan
  const yuborilmoqda = useNavigation().state === "submitting";
  const [soralmoqda, setSoralmoqda] = useState(false);
  …
      <Form method="post" className="qator">
        <input type="hidden" name="niyat" value="almashtir" />
        <input
          type="hidden"
          name="bajarildi"
          value={String(!vazifa.bajarildi)}
        />
        <Tugma type="submit" id="vazifa-almashtir" variant="asosiy">
          {yuborilmoqda
            ? "Saqlanmoqda…"
            : vazifa.bajarildi
              ? "Yana faol qilish"
              : "Bajarildi deb belgilash"}
        </Tugma>
      </Form>
      …
      <p id="vazifa-xabar" role="status">
        {natija?.xabar}
      </p>

«O'chirish…» birinchi bosilganda soralmoqda — true, «Ha, o'chirish» va «Bekor qilish» chiqadi. Ikkinchi forma niyat=ochir ni yuboradi. Natija xabari role="status" da — Malika eshitadi.

8.5 Marshrut va xato sahifasi

marshrutlar.tsx (kanon):

diff
     path: "/",
     element: <Qobiq />,
     errorElement: <XatoSahifasi />,
+    // 19/#05: sahifa birinchi ochilishida loader ishlayotgan
+    // bo'lsa (/vazifa/2 to'g'ridan) — router shuni ko'rsatadi.
+    // Ro'yxat sahifasida loader yo'q: u birinchi kadrda
+    // chiziladi (CLS 0, 13/#30)
+    hydrateFallbackElement: <p role="status">Yuklanmoqda…</p>,
...
       {
         path: "vazifa/:id",
         element: <VazifaSahifasi />,
-        children: [{ index: true, element: <VazifaUmumiy /> }],
+        loader: vazifaLoader,
+        errorElement: <VazifaXatosi />,
+        children: [
+          {
+            index: true,
+            element: <VazifaUmumiy />,
+            action: vazifaAction,
+          },
+        ],
       },

Nega action index bolada, otada emas? <Form> ga action atributi berilmasa, u o'zi turgan marshrutga yuboradi. Formalar VazifaUmumiy da — index bolada. Action otada bo'lsa, router "bu marshrutda action yo'q" deb 405 xatosini berardi.

VazifaXatosi.tsx — «Loader xato tashlasa» bo'limidagi DishError ning o'zi: useRouteError(), 404 bo'lsa sarlavha "Vazifa topilmadi", boshqa xatoda — "Vazifani ochib bo'lmadi". Matn javob-xatoda String(xato.data) dan, kutilmagan xatoda — xatoMatni(xato) (Error ning message i). Pastda «Ro'yxatga qaytish» havolasi. U vazifa/:id marshrutida — xato faqat sahifa qismini egallaydi, Qobiq (menyu, sarlavha) joyida qoladi.

8.6 ESLint: throw faqat Error emas

Birinchi npm run lint to'rtta xato berdi (natijalar/tekshiruv-05.txt):

text
   59:11  error  Expected an error object to be thrown  @typescript-eslint/only-throw-error

Tarjimasi: "Xato obyekti tashlanishi kutilgan edi". Bu qoida ESLint plaginlari dan — odatda foydali: throw "matn" stek izini yo'qotadi. Lekin router aynan throw data(…) va throw replace(…) ni kutadi. Kanon qoidani o'chirmadi, eslint.config.js da faqat ikki turga ruxsat berdi: allow: [{ from: "lib", name: "Response" }, { from: "package", package: "react-router", name: "DataWithResponseInit" }]. replace() va redirect() — Response, data() — DataWithResponseInit. Boshqa narsani tashlash hamon xato.

8.7 Tekshiruvlar

bash
git add -A
git commit

Commit (kanon): feat: vazifa sahifasi — loader, action (belgilash, o'chirish), errorElement.

  • npm test — node:test 182, Vitest 55 (+6: loader, 404, /vazifa/02, keshdan, action, server 500). Testda ham xotiradagi router: createMemoryRouter va soxta fetch.
  • lint, format:check, tip, knip — exit 0. Build: gzip: 114.49 kB (oldin 114.25).
  • Brauzer: 159/159, 72/72, 19-qism — 39/39. Yangi sakkiztasidan uchtasi:
text
✅ loader: to'g'ridan ochilganda serverdan (GET): ["Non olish #bozor",["GET"]]
✅ action: belgilandi — PATCH, keyin loader qayta (GET), sahifa yangi: [["GET","PATCH 1","GET"],"Bajarilgan","Bajarildi deb belgilandi."]
✅ errorElement: o'chirilgan vazifa — 404, menyu joyida: ["Vazifa topilmadi",3]

Ikkinchi qatorga qarang: GET, PATCH 1, GET. Bu — «Forma router'ga yuboriladi» bo'limidagi uch qatorli konsolning haqiqiy serverdagi ko'rinishi: loader, action, revalidatsiya. Uchinchisi: o'chirilgan vazifaning sahifasi 404, menyuda esa uch bo'lim (3) turibdi — xato faqat sahifa qismida. Diff: 7 fayl, +446 −50.

9. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
No \HydrateFallback` element provided…`, ekran bo'sh birinchi ochilishda loader ishlayapti hydrateFallbackElement
useLoaderData() — undefined loader boshqa marshrutda (otada) o'z marshrutiga loader yoki useRouteLoaderData(id)
action'dan keyin ro'yxat yangilanmadi qo'lda setState kutildi loader serverdan olsin — revalidatsiya o'zi yangilaydi
forma yuborilganda 405 <Form> turgan marshrutda action yo'q action'ni shu marshrutga yoki <Form action="…">
xato butun sahifani (menyuni ham) yutdi errorElement faqat ildizda errorElement ni sahifa marshrutiga
"Unexpected Application Error!" hech qayerda errorElement yo'q ildiz marshrutga o'z xato sahifasi
«Orqaga» aylanaga tushdi loader'da redirect (push) replace()
yoqtirish manzilni o'zgartirdi <Form> ishlatildi useFetcher

useNavigate ni action ichida chaqirish ham ko'p uchraydi: Invalid hook call. Action — oddiy funksiya, komponent emas. O'tish kerak bo'lsa — return redirect(…).

10. Mashqlar

Mashqlar kurs/mashqlar/19/05-data-mode/ da. Har yechimni «Ishga tushir» bilan sinang.

1-mashq (oson): kim, qachon?

Bo'sh joylarni to'ldiring:

  1. Sahifa uchun ma'lumot olish — marshrutning funksiyasida.
  2. Komponent bu ma'lumotni hook'i bilan oladi.
  3. <Form method="post"> yuborilganda router marshrutning funksiyasini chaqiradi.
  4. Action tugagach router loader'larni qayta chaqiradi. Bu deyiladi.
  5. Sahifadan ketmasdan amal bajarish uchun ishlatiladi.
Yechim

loader, useLoaderData, action, revalidatsiya, useFetcher. Eslab qolish uchun: loader — o'qish (GET), action — yozish (POST, PATCH, DELETE). HTTP metodlari kabi: <Form method="get"> loader'ga, method="post" action'ga boradi. Fetcher — xuddi shular, lekin navigatsiyasiz.

2-mashq (o'rta): effektdan loader'ga

Quyidagi sahifa narxni useEffect da oladi. Uni loader'ga o'tkazing:

  1. useState va useEffect olib tashlansin.
  2. Marshrutga loader — getPrice() ni qaytaradi.
  3. Birinchi ochilishda "Yuklanmoqda…" ko'rinsin (hydrateFallbackElement).

Ishora: «Birinchi loader» va «Birinchi ochilish» bo'limlari.

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

const getPrice = () =>
  new Promise((resolve) => setTimeout(() => resolve(35000), 250));

function Price() {
  const [price, setPrice] = useState(null);
  useEffect(() => {
    getPrice().then(setPrice);
  }, []);
  if (price === null) return <p>Yuklanmoqda…</p>;
  return <p>Osh: {price} so'm</p>;
}

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

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

Ekranda:

text
Osh: 35000 so'm
Yechim
jsx
import {
  createMemoryRouter,
  RouterProvider,
  useLoaderData,
} from "react-router";

const getPrice = () =>
  new Promise((resolve) => setTimeout(() => resolve(35000), 250));

function Price() {
  const price = useLoaderData();
  return <p>Osh: {price} so'm</p>;
}

const router = createMemoryRouter([
  {
    path: "/",
    element: <Price />,
    loader: getPrice,
    hydrateFallbackElement: <p>Yuklanmoqda…</p>,
  },
]);

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

Ekranda:

text
Osh: 35000 so'm

Price endi bir qator: ma'lumot doim tayyor. loader: getPrice — funksiyaning o'zi berildi, chaqiruvi emas (getPrice() emas). Router uni o'zi, kerakli paytda chaqiradi. "Yuklanmoqda…" endi komponentning ishi emas — marshrut darajasida.

3-mashq (qiyin): stol band qilish

/ sahifasi — stollar ro'yxati va band qilish formasi:

  1. Loader tables massivini qaytaradi; ekranda Band: 1 ta.
  2. Forma: name="guest" input (aria-label «Mehmon») va «Band qilish» tugmasi.
  3. Action: ism bo'sh bo'lsa — data({ error: "Ism kiriting" }, { status: 400 }); aks holda tables.push(guest) va redirect("/rahmat").
  4. /rahmat sahifasi: «Rahmat! Stol band qilindi.»

Ishora: «Xato qaytarish» va «Amaldan keyin boshqa sahifa» bo'limlari. Avval bo'sh yuborib xatoni, keyin ism bilan yuborib sinang.

jsx
import {
  createMemoryRouter,
  data,
  Form,
  redirect,
  RouterProvider,
  useActionData,
  useLoaderData,
} from "react-router";

const tables = ["Ali"];

// Booking, loader, action — shu yerga

const router = createMemoryRouter([
  { path: "/", element: <p>Stollar</p> },
  { path: "/rahmat", element: <p>Rahmat!</p> },
]);

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

Ekranda:

text
Stollar
Yechim
jsx
import {
  createMemoryRouter,
  data,
  Form,
  redirect,
  RouterProvider,
  useActionData,
  useLoaderData,
} from "react-router";

const tables = ["Ali"];

function Booking() {
  const list = useLoaderData();
  const result = useActionData();
  return (
    <Form method="post">
      <p>Band: {list.length} ta</p>
      <input name="guest" aria-label="Mehmon" />
      <button>Band qilish</button>
      {result?.error && <p role="alert">{result.error}</p>}
    </Form>
  );
}

const router = createMemoryRouter([
  {
    path: "/",
    element: <Booking />,
    loader: () => [...tables],
    action: async ({ request }) => {
      const guest = String((await request.formData()).get("guest"));
      if (guest.trim() === "") {
        return data({ error: "Ism kiriting" }, { status: 400 });
      }
      tables.push(guest.trim());
      return redirect("/rahmat");
    },
  },
  { path: "/rahmat", element: <p>Rahmat! Stol band qilindi.</p> },
]);

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

Ekranda:

text
Rahmat! Stol band qilindi.

Harakat qatori ikki holatni tekshiradi: bo'sh yuborilganda "Ism kiriting" (ekran qadami), ism bilan — tasdiq sahifasi. String(…) — formData.get maydon bo'lmasa null qaytaradi, String(null).trim() yiqilmaydi. loader: () => [...tables] — nusxa: React yangi massivni ko'rib, qayta chizadi. Xato holatida status 400 — loader keraksiz qayta chaqirilmaydi.

4-mashq: Vazifalar qadami

feat/vazifa-loader branch'ida «Vazifalar qadami» bo'limidagi o'zgarishlarni qiling: vazifa-marshruti.ts (loader, action), VazifaSahifasi.tsx — useLoaderData, VazifaUmumiy.tsx — ikki forma, VazifaXatosi.tsx, marshrutlar.tsx va eslint.config.js. Keyin commit.

Yechim

Tekshiruv ro'yxati (npm run dev, «Bahor» API ishlab turgan holda):

  • /vazifa/1 ni yangi tabda oching — bir zum "Yuklanmoqda…", keyin vazifa. DevTools Network'da bitta GET.
  • «Bajarildi deb belgilash» — tugma "Saqlanmoqda…", keyin "Yana faol qilish" va "Bajarildi deb belgilandi.". Network: PATCH, keyin GET.
  • «O'chirish…» → «Ha, o'chirish» — ro'yxat sahifasi, vazifa yo'q. «Orqaga» — "Vazifa topilmadi", menyu joyida.
  • /vazifa/02 — manzil /vazifa/2, brauzerning «Orqaga» tugmasi aylanaga tushmaydi.
  • Network'da "Offline" ni yoqib, /vazifa/2 ni yangilang — vazifa keshdan, konsolda Vazifa keshdan: ….
  • npm test — node 182, Vitest 55; npm run lint — xatosiz.

11. Real ishda

  • Remix va React Router framework mode — loader va action g'oyasi aynan shu yerdan chiqqan. Keyingi darsda ko'ramiz.
  • Next.js — server komponentlar va Server Actions: ma'lumot ham, o'zgartirish ham marshrutga bog'langan. Data mode'ni bilgan odam ularni tez tushunadi.
  • TanStack Query bilan birga — katta ilovalarda loader keshni to'ldiradi, komponent esa keshdan o'qiydi. vazifalar da shunday bo'ladi: TanStack Query I.
  • Intervyu savollari: "loader va useEffect bilan yuklash farqi?", "revalidatsiya nima?", "<Form> va useFetcher qachon?", "errorElement qayerga qo'yiladi?".

Xulosa

  • loader — sahifa chizilishidan oldin ma'lumot; komponent useLoaderData() bilan oladi. Ichma-ich loader'lar parallel — zinapoya yo'q.
  • Kutish holati — useNavigation().state (butun ilova), birinchi ochilishda — hydrateFallbackElement.
  • <Form method="post"> → action → revalidatsiya. Xato — useActionData() ga qaytariladi; 4xx/5xx status loader'larni qayta chaqirmaydi.
  • redirect — push, replace — almashtirish; action va loader'da useNavigate o'rniga.
  • useFetcher — navigatsiyasiz amal: o'z state, data, formData (optimistik UI).
  • Xato — throw data(…, { status }); uni eng yaqin errorElement useRouteError va isRouteErrorResponse bilan ko'rsatadi. errorElement — iloji boricha pastda.

Keyingi dars: React Router V: framework mode — data mode ustidagi Vite plagini: fayl asosidagi marshrutlar, avtomatik tiplar va SSR. vazifalar da sinab, o'lchaymiz.

Manbalar

  • React Router: Data Loading, Actions, Pending UI — reactrouter.com/start/data/… (2026-10 holatiga, v8.4.0)
  • React Router: useFetcher, useNavigation, useRouteError, isRouteErrorResponse, data, redirect, replace — reactrouter.com/api/…
  • React Router: Route Object (loader, action, errorElement, hydrateFallbackElement, lazy) — reactrouter.com/start/data/route-object
  • typescript-eslint: only-throw-error (allow) — typescript-eslint.io/rules/only-throw-error
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
React Router data mode: loader, action, Form, useFetcher va errorElement — IlmHamroh