IlmHamroh
JavaScript Full-stack/18-qism. React chuqur6/42-dars21 daqiqa
Mundarija (28)

useActionState va useFormStatus: Action natijasi, navbat va forma holati props'siz

Qisqacha: useActionState(action, initialState) — Action'ning natijasini state qiladi: [state, dispatch, isPending]. Action (prevState, payload) ni oladi va yangi state'ni qaytaradi. Bir necha yuborish bo'lsa, React ularni navbat bilan bajaradi. Keyingisi oldingisining natijasini prevState sifatida oladi. dispatch ni <form action> ga bering yoki startTransition ichida chaqiring. useFormStatus() (react-dom dan) — forma ichidagi komponentga o'z formasining holatini beradi: pending, data, method, action. Props kerak emas. Lekin formani chizgan komponentning o'zida u doim pending: false.

Bu darsda

  • prevState bilan natijani to'plab boradigan Action yozasiz (savat).
  • Action'lar navbatini konsolda ko'rasiz va nega bu oddiy transition'dan xavfsizroq ekanini tushuntirasiz.
  • dispatch ni formasiz, tugmadan chaqirasiz va React'ning ogohlantirishini o'qiysiz.
  • useFormStatus bilan qayta ishlatiladigan yuborish tugmasini yozasiz va "faqat forma ichida" qoidasini jonli ko'rasiz.
  • vazifalar dagi yangi vazifa formasini <form action> ga ko'chirasiz: xato bo'lsa matn joyida, tugma "Qo'shilmoqda…".

Oldin bilishingiz kerak: Actions, Formalar II: form action, Context I, useReducer.

1. Nega bu kerak?

Oldingi darsda Action'ni ko'rdik: kutish holatini React o'zi biladi, forma o'zi tozalanadi. Sardor bron formasini Action'ga aylantirdi va ikki savolga duch keldi.

Birinchisi — natija. Action "5-stol bron qilindi" yoki "bu stol band" deb javob berishi kerak. <form action> esa qaytgan qiymatni e'tiborsiz qoldiradi. Sardor yana useState va try/catch ga qaytdi.

Ikkinchisi — tugma. Dizayner «Bahor» uchun chiroyli SubmitButton komponentini chizdi: yuborilayotganda aylanadigan belgi va "Yuborilmoqda…" yozuvi bilan. U saytdagi 12 ta formada ishlatiladi. Har formada isPending ni olib, tugmaga prop qilib berish kerak bo'ldi. 12 joy — 12 ta imkoniyat unutishga.

Hayotiy o'xshatish: oshxonadagi buyurtma navbati. Ofitsiant buyurtmalarni oshpazga bittadan beradi. Oshpaz har yangi buyurtmani oldingisi tugagan stolning joriy holatiga qarab tayyorlaydi: "stolga 2 osh berilgan edi, endi yana 1 — jami 3". Bu — useActionState. Zaldagi tablo esa har stolga "buyurtmangiz tayyorlanmoqda" deb ko'rsatadi. Stol egasi oshpazdan so'rab yurmaydi — tablo o'zi biladi. Bu — useFormStatus.

2. useActionState chuqur

2.1 Imzo — qisqa takror

Formalar II darsida ko'rgansiz:

jsx
const [state, dispatch, isPending] = useActionState(
  action,        // (prevState, payload) => yangi state (async ham)
  initialState,  // birinchi renderdagi state
  permalink,     // ixtiyoriy — server framework'lar uchun
);
  • state — Action oxirgi marta qaytargan qiymat. Hali chaqirilmagan bo'lsa — initialState.
  • dispatch — o'ralgan Action. Uni <form action> ga yoki formAction ga bering, yoki startTransition ichida chaqiring. React hujjatlari uni ko'pincha formAction deb ataydi — ikkalasi bir narsa.
  • isPending — Action (yoki navbatdagilardan biri) bajarilayotganmi.

Bugun uchta yangi narsa: prevState nima uchun kerak, navbat va permalink.

2.2 prevState: natijani to'plash

Action birinchi argument sifatida oldingi natijani oladi. Shuning uchun u reducer ga juda o'xshaydi: (holat, amal) => yangi holat. Farqi — Action async bo'la oladi va serverga borib kela oladi.

«Bahor» savati: har «Savatga» bosilganda taom serverga yoziladi va ro'yxatga qo'shiladi:

jsx
import { useActionState } from "react";

async function addToCart(prevCart, formData) {
  await new Promise((resolve) => setTimeout(resolve, 100));
  const dish = formData.get("dish").trim();
  if (dish === "") return prevCart; // o'zgarishsiz
  return [...prevCart, dish];
}

export default function App() {
  const [cart, formAction, isPending] = useActionState(addToCart, []);
  return (
    <form action={formAction}>
      <label>
        Taom <input name="dish" />
      </label>
      <button disabled={isPending}>Savatga</button>
      <p>Savatda: {cart.length > 0 ? cart.join(", ") : "bo'sh"}</p>
    </form>
  );
}

Ekranda:

text
Taom Savatga
Savatda: Osh, Manti

Komponentda useState yo'q. Savat — Action natijasi, Action esa uni prevCart dan davom ettiradi. Bo'sh nom kelsa — prevCart o'zgarishsiz qaytadi: state o'sha massiv, ekran o'zgarmaydi.

2.3 Navbat: Action'lar ketma-ket

Oldingi darsda ikki marta bosilgan oddiy Action'lar parallel ishladi — ikkita so'rov bir vaqtda yo'lda edi. useActionState boshqacha. Ikki marta tez bosamiz:

jsx
import { useActionState } from "react";

let started = 0;

async function addDish(prevCount) {
  const id = ++started;
  console.log(`${id}-so'rov boshlandi, oldingi: ${prevCount}`);
  await new Promise((resolve) => setTimeout(resolve, 200));
  console.log(`${id}-so'rov tugadi`);
  return prevCount + 1;
}

export default function App() {
  const [count, formAction, isPending] = useActionState(addDish, 0);
  console.log("render:", count, isPending);
  return (
    <form action={formAction}>
      <button>+1 osh</button>
      <p>Buyurtmada: {count} osh</p>
    </form>
  );
}

Konsolda:

text
render: 0 false
1-so'rov boshlandi, oldingi: 0
render: 0 true
render: 0 true
1-so'rov tugadi
2-so'rov boshlandi, oldingi: 1
2-so'rov tugadi
render: 2 false

Ekranda:

text
+1 osh
Buyurtmada: 2 osh

Konsolni o'qiymiz:

Qadam Nima bo'ldi
1-bosish 1-so'rov darhol boshlandi, isPending — true
2-bosish 2-so'rov boshlanmadi — navbatda turibdi
1-so'rov tugadi natija 1 — lekin ekranga chiqmadi
2-so'rov boshlandi prevCount — 1: oldingisining natijasi
2-so'rov tugadi bitta render: 2, isPending: false

Ikki xulosa. Birinchisi: 2-so'rov 1-sining natijasini ko'rdi. Agar ular parallel bo'lganda, ikkalasi ham prevCount: 0 ni olib, 1 qaytarardi — bitta osh yo'qolardi. Ikkinchisi: oraliq natija (1) ekranga chiqmadi — navbat tugaguncha React kutdi. Oldingi darsdagi "Action yangilanishlari birga chiziladi" qoidasi navbatga ham tegishli.

Navbatning narxi — tezlik: 10 ta bosish = 10 ta ketma-ket so'rov. Ko'pincha bu to'g'ri tanlov: server tartibni to'g'ri ko'radi. Tezlik kerak bo'lsa — keyingi darsdagi useOptimistic: ekran darhol, so'rovlar fonda.

Tekshirib ko'ring: Ikkinchi render: 0 true qatori qayerdan chiqdi? Hech narsa o'zgarmagan-ku.

Javob

Birinchi render: 0 true — bosish paytidagi shoshilinch render: React faqat isPending ni yangiladi. Ikkinchisi — transition renderi: React Action'ni boshladi, lekin u await da kutyapti. Actions darsidagi "Saqlanmoqda…" misolida ham xuddi shu to'rt qator bor edi. Ikkinchi bosish yangi render chaqirmadi — u navbatga yozildi, xolos.

2.4 Formasiz: dispatch tugmadan

dispatch ga FormData berish shart emas. Ikkinchi argument — istalgan payload: id, obyekt, satr. Savatni tugmalar bilan to'ldiramiz:

jsx
import { useActionState } from "react";

async function addToCart(prevCart, dish) {
  await new Promise((resolve) => setTimeout(resolve, 100));
  return [...prevCart, dish];
}

export default function App() {
  const [cart, dispatch] = useActionState(addToCart, []);
  return (
    <div>
      <button onClick={() => dispatch("Osh")}>Osh</button>
      <p>Savatda: {cart.join(", ")}</p>
    </div>
  );
}

Konsolda:

text
An async function with useActionState was called outside of a transition. This is likely not what you intended (for example, isPending will not update correctly). Either call the returned function inside startTransition, or pass it to an `action` or `formAction` prop.

Ekranda:

text
Osh
Savatda: Osh

Savat ishladi, lekin React ogohlantirdi: "useActionState dagi async funksiya transition'dan tashqarida chaqirildi. Ehtimol siz buni xohlamagansiz (masalan, isPending to'g'ri yangilanmaydi). Qaytgan funksiyani startTransition ichida chaqiring yoki action yoki formAction prop'iga bering."

<form action> da React transition'ni o'zi ochadi. Oddiy onClick da esa — siz:

jsx
import { startTransition, useActionState } from "react";

async function addToCart(prevCart, dish) {
  await new Promise((resolve) => setTimeout(resolve, 100));
  return [...prevCart, dish];
}

export default function App() {
  const [cart, dispatch, isPending] = useActionState(addToCart, []);
  function handleClick() {
    startTransition(() => dispatch("Osh")); // ✅
  }
  return (
    <div>
      <button onClick={handleClick}>Osh</button>
      <p>
        {isPending ? "Qo'shilmoqda…" : `Savatda: ${cart.join(", ")}`}
      </p>
    </div>
  );
}

Ekranda:

text
Osh
Savatda: Osh

Ogohlantirish yo'q, isPending esa kutish paytida "Qo'shilmoqda…" ni ko'rsatdi (ekran bosishdan 60 ms keyin tekshirdi).

2.5 Xato: qaytaring, tashlamang

Action throw qilsa — Actions darsidagidek, eng yaqin error boundary'ga ketadi. Bundan tashqari, navbatda turgan keyingi yuborishlar ham bekor qilinadi: React hujjati (react.dev, useActionState, 2026-10) shunday deydi. Buni o'zimiz tekshiramiz: uch marta tez bosamiz, birinchi so'rov esa xato bilan tugaydi. Konsolda nechta "boshlandi" qatori chiqishiga qarang:

jsx
import { useActionState } from "react";

let started = 0;

async function addDish(prevCount) {
  const id = ++started;
  console.log(`${id}-so'rov boshlandi`);
  await new Promise((resolve) => setTimeout(resolve, 200));
  if (id === 1) throw new Error("500: server xatosi");
  return prevCount + 1;
}

export default function App() {
  const [count, formAction] = useActionState(addDish, 0);
  return (
    <form action={formAction}>
      <button>+1 osh</button>
      <p>Buyurtmada: {count} osh</p>
    </form>
  );
}

Konsolda:

text
1-so'rov boshlandi
Uncaught Error: 500: server xatosi

Nima bo'ldi? Uchta bosish bo'ldi, lekin faqat bitta so'rov boshlandi. 2- va 3-yuborish navbatda kutib turgan edi. 1-so'rov throw qilgach, ular umuman boshlanmadi — xato bilan birga yo'qoldi. Error boundary bo'lmagani uchun ilova ham o'chdi. Foydalanuvchi uchun bu ikki marta yomon: xato ekrani va yo'qolgan ikki buyurtma.

Shuning uchun kutilgan xatolarni state'ning bir qismi qilib qaytaring:

jsx
import { useActionState } from "react";

async function book(prevState, formData) {
  await new Promise((resolve) => setTimeout(resolve, 100));
  const name = formData.get("name").trim();
  if (name.length < 3) {
    return { error: "Ism kamida 3 harf", name }; // yozilgani qaytadi
  }
  const done = `${name}, stol bron qilindi`;
  return { error: null, name: "", done };
}

const INITIAL = { error: null, name: "", done: null };

export default function App() {
  const [state, formAction] = useActionState(book, INITIAL);
  return (
    <form action={formAction}>
      <label>
        Ism <input name="name" defaultValue={state.name} />
      </label>
      <button>Bron qilish</button>
      {state.error && <p role="alert">{state.error}</p>}
      {state.done && <p>{state.done}</p>}
    </form>
  );
}

Ekranda:

text
Ism Bron qilish
Ism kamida 3 harf

Bu Formalar II dagi usul: defaultValue={state.name}. React Action'dan keyin formani reset qiladi, maydon esa "Al" ga qaytadi — xato bilan qaytgan natijadagi qiymatga. Muvaffaqiyatda name: "" — maydon bo'shaydi. role="alert" — Malika xatoni darhol eshitadi.

Uchinchi argument — URL satri. U faqat server komponentlar bilan ishlaydigan framework'da (Next.js) ma'noga ega. Sahifa serverdan HTML bo'lib keldi, JavaScript esa hali yuklanmagan. Foydalanuvchi formani shu paytda yuborsa, brauzer uni permalink manziliga oddiy HTML forma kabi jo'natadi. Server javobni o'sha sahifa bilan qaytaradi.

jsx
// Next.js: JS yuklanmasdan oldin ham forma ishlaydi
const [state, formAction] = useActionState(
  bookTable,
  INITIAL,
  "/bron",
);

Bunday "JS'siz ham ishlash" progressive enhancement (bosqichma-bosqich yaxshilash) deyiladi. Bizning Vite ilovamizda server komponent yo'q — permalink hech narsa qilmaydi. To'liq — Server Actions II darsida.

3. useFormStatus

3.1 Tugma o'z formasini biladi

useFormStatus — react-dom dagi hook. U ota <form> ning hozirgi yuborilish holatini qaytaradi. Dizaynerning tugmasi:

jsx
import { useFormStatus } from "react-dom";

function SubmitButton() {
  const { pending, data, method } = useFormStatus();
  console.log("SubmitButton:", pending, method, data?.get("name"));
  const name = data?.get("name");
  return (
    <button disabled={pending}>
      {pending ? `${name} uchun yuborilmoqda…` : "Yuborish"}
    </button>
  );
}

async function book(formData) {
  await new Promise((resolve) => setTimeout(resolve, 200));
}

export default function App() {
  return (
    <form action={book}>
      <label>
        Ism <input name="name" />
      </label>
      <SubmitButton />
    </form>
  );
}

Konsolda:

text
SubmitButton: false null undefined
SubmitButton: true get Malika
SubmitButton: false null undefined

Ekranda:

text
Ism Yuborish

SubmitButton hech qanday prop olmadi. Lekin u bildi: forma yuborilyapti (pending), qaysi ma'lumot bilan (data — o'sha FormData) va qaysi usulda (method). Bu book oddiy forma Action'i — useActionState siz ham ishladi. 200 ms dan keyin hammasi boshlang'ich holatga qaytdi.

useFormStatus() qaytaradigan obyekt:

Maydon Yuborilmaganda Yuborilayotganda
pending false true
data null yuborilgan FormData
method null "get" yoki "post" (forma atributidan)
action null formaning action i (funksiya yoki URL)

method nega "get"? Formaga method atributi yozilmagan, HTML'da esa standart — GET. React funksiya-Action'ni baribir o'zi chaqiradi, method faqat ma'lumot uchun.

Ichkarida bu Context ga o'xshaydi: <form> holatini "ta'minlaydi", ichidagi komponentlar uni o'qiydi. Farqi — Provider ni siz yozmaysiz, uni <form> ning o'zi beradi.

3.2 Faqat forma ichida

Eng ko'p uchraydigan tuzoq: hook'ni formani chizgan komponentning o'zida chaqirish:

jsx
import { useFormStatus } from "react-dom";

async function book(formData) {
  await new Promise((resolve) => setTimeout(resolve, 200));
}

export default function App() {
  // ❌ forma — pastda, App ichida emas
  const { pending } = useFormStatus();
  console.log("App pending:", pending);
  return (
    <form action={book}>
      <button disabled={pending}>
        {pending ? "Yuborilmoqda…" : "Yuborish"}
      </button>
    </form>
  );
}

Konsolda:

text
App pending: false

Bir marta chizildi — va tamom. Forma yuborildi, book 200 ms ishladi, lekin App qayta chizilmadi ham. useFormStatus yuqoriga qaraydi: chaqirilgan komponentning ota-bobolari ichidan <form> qidiradi. App esa formaning ichida emas — u formani yaratadi. Ota forma yo'q — holat doim "yuborilmagan".

Tuzatish — tugmani alohida komponentga ajratish («Tugma o'z formasini biladi» bo'limidagidek). Shuning uchun useFormStatus deyarli doim kichik "bola" komponentda yashaydi.

flowchart TD
  A["App: useFormStatus — ota forma yo'q"] --> F["form action={book}"]
  F --> I["input"]
  F --> B["SubmitButton: useFormStatus — ota forma bor"]
  F -. "holat" .-> B

Diagrammada uzuq chiziqqa qarang: forma holati faqat pastga, o'z ichidagilarga uzatiladi. App undan yuqorida turibdi.

Tekshirib ko'ring: Sahifada ikki forma bor, ikkalasida ham <SubmitButton />. Birinchi forma yuborilyapti. Ikkinchi formadagi tugmada pending qanday?

Javob

false. Har SubmitButton faqat o'z ota formasini ko'radi. Context kabi: eng yaqin ta'minlovchi — o'z formasi. Shuning uchun bitta SubmitButton komponentini 12 ta formada xavfsiz ishlatish mumkin — ular bir-birining holatini ko'rmaydi.

3.3 Qaysi biri qachon

isPending endi uch joydan keladi. Tanlov — qayerda kerakligiga qarab:

Vosita Nimani biladi Qayerda ishlatiladi
useTransition o'zi boshlagan transition'lar startTransition chaqirgan komponentda
useActionState shu Action'ning natijasi va navbati formani chizgan komponentda
useFormStatus ota formaning yuborilishi forma ichidagi bola komponentda

Ko'pincha ikkalasi birga: forma komponenti — useActionState (natija, xato), uning ichidagi tugma — useFormStatus (kutish). vazifalar formasi aynan shunday.

4. Vazifalar qadami: yangi vazifa formasi — <form action> (2-qism)

4.1 17-qismdagi qaror

Formalar II darsida vazifalar formasi <form action> emas, onSubmit bilan yozilgan edi. Sabab o'lchangan: React Action'dan keyin formani tozalardi va xato bo'lsa ham matn yo'qolardi. Endi bizda ikki yangi narsa bor: useActionState natijasidagi matn va useFormStatus. Shu ikkalasi bilan qaror qayta ko'rildi — <form action> kanonga kirdi.

4.2 Action natijasi: FormaHolati

YangiVazifaForma.tsx — kanondan aynan (izohlar telefonga sig'ishi uchun qatorlarga qayta bo'lingan):

tsx
// Action natijasi (useActionState state'i): xato va maydonda
// QOLADIGAN matn. React action tugagach formani O'ZI tozalaydi
// (reset — maydon defaultValue'ga qaytadi); defaultValue — shu matn:
// xato bo'lsa yozilgan matn joyida qoladi, qo'shilsa — bo'sh.
// 17/#19 da aynan shu muammo (reset matnni o'chirardi)
// <form action> ni kanondan chiqargan edi
type FormaHolati = {
  readonly xato: string | null;
  readonly matn: string;
};

Va Action'ning o'zi:

tsx
  // useActionState (18/#06): action natijasi (oldingi state →
  // yangisi). Bir necha yuborish — navbat bilan (biri tugagach
  // keyingisi)
  const [holat, yuborish] = useActionState(
    async (
      _oldingi: FormaHolati,
      malumot: FormData,
    ): Promise<FormaHolati> => {
      const xom = malumot.get("matn");
      const yozilgan = typeof xom === "string" ? xom : "";
      const natija = await onQosh(yozilgan.trim());
      switch (natija.tur) {
        case "xato":
          return { xato: natija.xabar, matn: yozilgan };
        case "saqlanmadi":
          // internet yo'q yoki server rad etdi — matn maydonda
          // qoladi
          return { xato: null, matn: yozilgan };
        case "qoshildi":
          setTakliflar([]);
          return { xato: null, matn: "" };
        default: {
          const qolmadi: never = natija;
          return qolmadi;
        }
      }
    },
    { xato: null, matn: boshlangichMatn },
  );

Har qismni o'qiymiz:

  • _oldingi — prevState, lekin bu formaga kerak emas: har yuborish mustaqil. Pastki chiziq — "ataylab ishlatilmaydi" belgisi (ESLint uni kechiradi).
  • await onQosh(...) — App'dagi vazifaniQosh endi async: server javobigacha kutadi (useOptimistic da ko'ramiz). Forma shu paytgacha "kutilmoqda".
  • Uch natija — uch state. Xato bo'lsa va saqlanmasa — matn: yozilgan, qo'shilsa — matn: "". never — discriminated union dagi to'liqlik tekshiruvi: yangi tur qo'shilsa, tsc shu yerda xato beradi.
  • Boshlang'ich state — { xato: null, matn: boshlangichMatn }: sahifa yangilanganda qoralama tiklanadi (16-qismdan).

Maydon esa natijadagi matnni oladi:

diff
-          defaultValue={boshlangichMatn}
+          defaultValue={holat.matn}

onSubmit + preventDefault + maydon.value = "" o'rniga — <form id="yangi-forma" noValidate action={yuborish}>.

4.3 Tugma — useFormStatus

tsx
// useFormStatus (18/#06): o'z formasining holati — props'siz.
// Faqat <form> ICHIDAGI komponentda ishlaydi (forma o'zi emas).
// Tugma bloklanmaydi (optimistik: keyingi vazifani yozish mumkin),
// faqat matni
function QoshishTugmasi() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" id="qoshish-tugma">
      {pending ? "Qo'shilmoqda…" : "Qo'shish"}
    </button>
  );
}

E'tibor bering: disabled yo'q. Bron formasida ikki marta bosishdan himoya kerak edi. Bu yerda esa vazifa ekranda darhol paydo bo'ladi (optimistik — keyingi dars) va foydalanuvchi keyingisini yozishi mumkin. Navbat esa ularni server tartibida yuboradi.

4.4 Xato tuzatilsa — yo'qolsin

Eski kodda xato useState edi — tuzatilsa setXato(null). Endi xato — Action natijasining bir qismi, uni to'g'ridan-to'g'ri o'zgartirib bo'lmaydi. Kanondagi yechim — qaysi natijaning xatosi "tuzatilgan"ini eslash:

tsx
  // Xato tuzatilsa — yo'qolsin: qaysi natijaning xatosi
  // "tuzatilgan"
  const [tuzatilgan, setTuzatilgan] = useState<FormaHolati | null>(
    null,
  );
  const xato = tuzatilgan === holat ? null : holat.xato;

Matn o'zgarib, tekshiruvdan o'tsa — setTuzatilgan(holat). Keyingi yuborish yangi holat obyektini beradi va u tuzatilgan ga teng emas — yangi xato yana ko'rinadi. xato — hosilaviy qiymat, alohida state emas.

Fokus ham o'zgardi. Eski kodda xato bo'lsa maydon.focus() hodisa ichida chaqirilardi. Endi natija Action tugagach keladi — hodisa allaqachon o'tib ketgan. Shuning uchun fokus — effektda:

tsx
  // Xato bilan qaytgan natija — fokus maydonga («Qo'shish»
  // bosilgan bo'lsa ham). Natija action tugagach keladi — hodisada
  // emas
  useLayoutEffect(() => {
    if (holat.xato !== null) {
      maydonRef.current?.focus();
    }
  }, [holat]);

useLayoutEffect — bo'yashdan oldin: Bobur fokus sakrashini ko'rmaydi.

4.5 Test va brauzer

tekshiruv/YangiVazifaForma.test.tsx ga ikki test qo'shildi. Xato bilan qaytsa — React formani tozaladi, lekin maydonda non OLISH qoldi (maydon.defaultValue ham shu). Server kutilayotganda tugma Qo'shilmoqda…, matn hali maydonda; javobdan keyin — Qo'shish va bo'sh maydon. Action async bo'lgani uchun yuborish await act(async () => { fireEvent.submit(...) }) ichida — natija Action tugagach chiziladi.

Haqiqiy Chrome'da (e2e-18, soxta server, kechikish va 500):

text
✅ forma: kutilmoqda — tugma (useFormStatus), matn hali maydonda: ["Qo'shilmoqda…","Guruch olish"]
✅ forma: tasdiqlandi — maydon tozalandi (React reset), id serverdan (100): ["",true]
✅ forma: rad etildi — vazifa yo'q, matn maydonda qoldi (useActionState): [false,"Yo'qolmaydigan matn"]

17-qismda <form action> ni chiqargan muammo — "rad etildi, matn yo'qoldi" — endi yo'q. Commit — useOptimistic darsida.

5. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
useFormStatus doim pending: false formani chizgan komponentda chaqirilgan tugmani alohida komponentga ajrating
An async function with useActionState was called outside of a transition dispatch oddiy onClick da startTransition(() => dispatch(...))
ikkinchi yuborish "kech" boshlanadi useActionState navbati kutilgan xulq; tezlik kerak bo'lsa — useOptimistic
prevState ishlatilmasa ham bor imzo (prevState, payload) birinchi argumentni _ bilan boshlang
xato bo'lsa maydon bo'shaydi natijada yozilgan qiymat yo'q { error, name } qaytaring, defaultValue={state.name}
xatodan keyin navbatdagi yuborishlar yo'qoldi Action throw qildi — navbat bekor kutilgan xatoni qaytaring, throw — faqat kutilmagani uchun
data.get(...) — Cannot read properties of null pending: false paytida data — null data ni faqat pending da o'qing yoki data?.get

6. Mashqlar

Mashqlar kurs/mashqlar/18/06-useactionstate/ da. Har yechimni «Ishga tushir» bilan sinang.

1-mashq (oson): qaysi vosita?

Har holat uchun useActionState, useFormStatus yoki useTransition deb yozing.

  • Forma ichidagi umumiy SubmitButton kutish paytida aylanadigan belgi ko'rsatsin.
  • Server "bu stol band" deb javob bersa, forma ostida shu matn chiqsin.
  • Formasiz «Yangilash» tugmasi ro'yxatni serverdan qayta olsin va kutish paytida tugma xira bo'lsin.
Yechim
  • useFormStatus — tugma forma ichida, holatni props'siz o'qiydi («Tugma o'z formasini biladi» bo'limi).
  • useActionState — Action natijasi (xato matni) state bo'ladi va ekranda chiziladi.
  • useTransition — forma yo'q, natija ham state'da emas; faqat "kutilmoqda" kerak (Actions).

2-mashq (o'rta): qayta ishlatiladigan tugma

Boshlang'ich kodda "Yuborilmoqda…" hech qachon ko'rinmaydi. SubmitButton komponentini yozing: useFormStatus bilan kutish paytida disabled va "Yuborilmoqda…", aks holda "Yuborish". Uni App dagi tugma o'rniga qo'ying. Ishora: «Faqat forma ichida» bo'limi.

jsx
import { useFormStatus } from "react-dom";

async function sendReview(formData) {
  await new Promise((resolve) => setTimeout(resolve, 200));
}

export default function App() {
  const { pending } = useFormStatus();
  return (
    <form action={sendReview}>
      <label>
        Sharh <input name="review" />
      </label>
      <button disabled={pending}>
        {pending ? "Yuborilmoqda…" : "Yuborish"}
      </button>
    </form>
  );
}

Ekranda:

text
Sharh Yuborish
Yechim
jsx
import { useFormStatus } from "react-dom";

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button disabled={pending}>
      {pending ? "Yuborilmoqda…" : "Yuborish"}
    </button>
  );
}

async function sendReview(formData) {
  await new Promise((resolve) => setTimeout(resolve, 200));
}

export default function App() {
  return (
    <form action={sendReview}>
      <label>
        Sharh <input name="review" />
      </label>
      <SubmitButton />
    </form>
  );
}

Ekranda:

text
Sharh Yuborish

ekran("Yuborilmoqda…") bosilgandan 60 ms keyin tekshiradi — SubmitButton forma ichida, pending ni ko'rdi. 200 ms dan keyin — yana "Yuborish". App dan useFormStatus olib tashlandi: u yerda hech narsa bermasdi.

3-mashq (qiyin): stol bron qilish — natija va navbat

useActionState bilan bron formasi: mehmonlar soni (name="guests"). Action (100 ms "server"):

  1. Son 1–12 oralig'ida bo'lmasa — role="alert" da "1 dan 12 gacha", yozilgan son maydonda qolsin.
  2. To'g'ri bo'lsa — jami bron qilingan o'rinlar oldingi natijaga qo'shilsin: "Bron qilingan o'rinlar: 4", keyin yana 2 — "6". Maydon bo'shasin.
  3. Tugma — alohida BookButton (useFormStatus): kutish paytida "Bron qilinmoqda…".

Ishora: state — { total, error, guests }; defaultValue={state.guests}; «prevState: natijani to'plash» va «Xato: qaytaring, tashlamang» bo'limlari.

Yechim
jsx
import { useActionState } from "react";
import { useFormStatus } from "react-dom";

async function book(prev, formData) {
  await new Promise((resolve) => setTimeout(resolve, 100));
  const raw = formData.get("guests");
  const guests = Number(raw);
  if (!Number.isInteger(guests) || guests < 1 || guests > 12) {
    return { ...prev, error: "1 dan 12 gacha", guests: raw };
  }
  return { total: prev.total + guests, error: null, guests: "" };
}

function BookButton() {
  const { pending } = useFormStatus();
  return (
    <button>{pending ? "Bron qilinmoqda…" : "Bron qilish"}</button>
  );
}

const INITIAL = { total: 0, error: null, guests: "" };

export default function App() {
  const [state, formAction] = useActionState(book, INITIAL);
  return (
    <form action={formAction}>
      <label>
        Mehmonlar{" "}
        <input name="guests" defaultValue={state.guests} />
      </label>
      <BookButton />
      <p>Bron qilingan o'rinlar: {state.total}</p>
      {state.error && <p role="alert">{state.error}</p>}
    </form>
  );
}

Ekranda:

text
Mehmonlar Bron qilish
Bron qilingan o'rinlar: 4
1 dan 12 gacha

Birinchi yuborish: 4 — to'g'ri, total 0 + 4. Ikkinchisi: 20 — xato, ...prev bilan total o'zgarmay qoldi, guests: "20" maydonga qaytdi. Endi 2 yozib yuborsangiz — 6 va xato yo'qoladi. prev bo'lmaganda total ni alohida useState da saqlash va ikki state'ni moslab turish kerak bo'lardi.

4-mashq: Vazifalar qadami

refactor/actions-optimistik branch'ida YangiVazifaForma.tsx ni «Vazifalar qadami» bo'limi bo'yicha o'zgartiring: FormaHolati, useActionState, QoshishTugmasi, defaultValue={holat.matn}, tuzatilgan va fokus effekti. onQosh turini Promise<QoshishNatijasi> ga o'zgartiring. Testlarni await act ga o'tkazing.

Yechim

Tekshiruv ro'yxati:

  • <form ... action={yuborish}> — onSubmit va preventDefault yo'q.
  • Vitest: yangi ikki test (xato — matn joyida, useFormStatus — "Qo'shilmoqda…") o'tadi. Kanonda bu bosqichda Vitest 33 ta.
  • Qo'lda: npm run dev da takror vazifani yozing — xato chiqadi, matn maydonda, fokus maydonda.
  • Commit hali yo'q — useOptimistic darsida, App bilan birga.

7. Real ishda

  • Next.js Server Actions — useActionState(serverAction, initialState) — eng ko'p uchraydigan juftlik: validatsiya xatolari serverdan state bo'lib qaytadi (Server Actions II). permalink va progressive enhancement aynan shu yerda ishlaydi.
  • Dizayn tizimlari — SubmitButton, FormSpinner kabi komponentlar useFormStatus bilan yoziladi: har formaga prop uzatish kerak emas.
  • React Hook Form — katta formalarda (o'nlab maydon, maydon darajasidagi xatolar) ko'pincha shu kutubxona; u ham React 19 Action'lari bilan ishlay oladi (React Hook Form I).
  • Intervyu savollari: "useActionState nimani qaytaradi va prevState nima uchun?", "nega useFormStatus formani chizgan komponentda ishlamaydi?", "bir necha yuborish bo'lsa nima bo'ladi?".

Xulosa

  • useActionState(action, initial) → [state, dispatch, isPending]; Action — (prevState, payload) => yangi state.
  • Yuborishlar navbat bilan: keyingisi oldingisining natijasini oladi, oraliq natijalar chizilmaydi.
  • dispatch — <form action>, formAction yoki startTransition ichida; aks holda ogohlantirish.
  • Kutilgan xato — state'da qaytariladi (defaultValue bilan matn saqlanadi); throw — navbatni bekor qiladi.
  • useFormStatus — pending, data, method, action; faqat forma ichidagi komponentda.
  • vazifalar: forma — <form action> + useActionState({ xato, matn }) + QoshishTugmasi (2-qism, commit — #07).

Keyingi dars: useOptimistic — server javobini kutmasdan natijani ko'rsatish va rad etilsa React o'zi qaytarishi.

Manbalar

  • React: useActionState — react.dev/reference/react/useActionState (2026-10 holati, React 19.3)
  • React: useFormStatus — react.dev/reference/react-dom/hooks/useFormStatus
  • React blog: "React v19" (2024-12-05) — useActionState, useFormStatus — react.dev/blog/2024/12/05/react-19
  • MDN: FormData — developer.mozilla.org/docs/Web/API/FormData
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
useActionState va useFormStatus: Action natijasi, navbat va forma holati props'siz — IlmHamroh