IlmHamroh
JavaScript Full-stack/23-qism. Next.js22/43-dars20 daqiqa
Mundarija (29)

Next.js Server Actions: useActionState, Zod validatsiya va optimistik UI

Qisqacha: Server Action natija qaytaradi, useActionState(action, boshlangich) esa uni komponent holatiga aylantiradi: const [holat, amal] = useActionState(xabarYubor, { holat: "bosh" }) va <form action={amal}>. Action'ning imzosi o'zgaradi: birinchi argument — oldingi holat, ikkinchisi — FormData. Serverga kelgan ma'lumot Zod sxemasi bilan tekshiriladi va xatolar maydon bo'yicha qaytariladi: { holat: "xato", xatolar: { email: "…" }, qiymatlar }. useFormStatus() tugmaga "yuborilmoqda" holatini va yuborilayotgan FormData ni beradi. useOptimistic UI'ni javobdan oldin yangilaydi — lekin formani client funksiyaga o'rasa, JavaScript'siz forma ishlamay qoladi (biz o'lchadik).

Bu darsda

  • useActionState bilan action natijasini (xato yoki muvaffaqiyat) formada ko'rsatasiz.
  • Kanondagi Zod sxemasini natija oynasida jonli sinaysiz va xatolarni maydonlarga taqsimlaysiz.
  • Xato qaytarish shaklini (discriminated union) va kiritilgan qiymatlarni saqlashni loyihalaysiz.
  • useFormStatus ning pending va data maydonlarini ishlatasiz.
  • useOptimistic ning haqiqiy narxini ko'rasiz: JS'siz formada POST umuman ketmadi — shuning uchun kanon uni olmadi.
  • React Hook Form va next-safe-action qachon kerakligini bilasiz.

Oldin bilishingiz kerak: Server Actions I, useActionState va useFormStatus, useOptimistic, RHF + Zod: resolver va umumiy sxemalar, Discriminated union.

1. Nega bu kerak?

Oldingi darsda forma serverga yetib bordi. Lekin foydalanuvchi uchun u hali "qora quti": yuborildimi? Email noto'g'ri bo'lsa nima bo'ladi? Xabar 3 harf bo'lsa-chi?

Brauzer tekshiruvi (required, type="email", minLength) qulay, lekin himoya emas. Uni DevTools'da bir bosishda o'chirish mumkin, curl bilan esa umuman chetlab o'tiladi. Kanon buni sinab ko'rgan: UI'siz, to'g'ridan POST bilan mavzu=reklama yuborilganda server baribir qabul qilib tekshirdi. Haqiqiy tekshiruv — faqat serverda.

Demak uchta narsa kerak:

  1. Serverda ishonchli tekshiruv — Zod.
  2. Xatoni foydalanuvchiga qaytarish — qaysi maydon, nima xato.
  3. Kutish paytida aniq holat — "yuborilmoqda…".

Bankdagi ariza oynasini eslang. Siz arizani topshirasiz. Xodim tekshiradi va "pasport raqami yo'q" deb qaytaradi — lekin qolgan to'ldirganlaringizni yirtib tashlamaydi, faqat xato joyni belgilaydi. Bizning forma ham shunday bo'lishi kerak.

2. Zod: serverdagi qo'riqchi

2.1 Kanon sxemasi

19-qismda Zod'ni React Hook Form bilan brauzerda ishlatgansiz. Bu yerda u serverda turadi. Kanon aloqa/_amal/sxema.ts:

ts
import { z } from "zod";

// Aloqa formasi sxemasi (N-22). Faqat serverda ishlatiladi —
// brauzer cheklovlari (required, maxlength) — qulaylik,
// himoya EMAS.
export const XABAR_SXEMASI = z.object({
  ism: z
    .string()
    .trim()
    .min(2, "Ismingizni yozing (kamida 2 harf)")
    .max(60, "Ism 60 belgidan oshmasin"),
  email: z.email("Email manzili noto'g'ri").max(120),
  mavzu: z.enum(["ish", "savol"], "Mavzuni tanlang"),
  xabar: z
    .string()
    .trim()
    .min(10, "Xabar kamida 10 belgi bo'lsin")
    .max(2000, "Xabar 2000 belgidan oshmasin"),
});

export type Xabar = z.infer<typeof XABAR_SXEMASI>;
export type Maydon = keyof Xabar;

Kanon izohi telefonga sig'ishi uchun bo'lindi.

Qator Ma'nosi
.trim() Avval bo'shliqlar olib tashlanadi — " " bo'sh hisoblanadi
z.email(…) Zod 4 da email — alohida tur (z.string().email() eskirgan)
z.enum([…], "…") Faqat ikki qiymat; ikkinchi argument — xato matni
.max(2000) Yuqori chegara — 1 MB xabar bilan serverni band qilib bo'lmaydi
z.infer Sxemadan TypeScript turi — qo'lda yozmaymiz (Zod va tur)

Nega sxema alohida faylda, action ichida emas? Oldingi darsda ko'rdik: "use server" fayldan faqat async funksiya eksport qilinadi. Sxema va turlar boshqa fayllarga ham kerak (forma komponenti turlarni ishlatadi) — shuning uchun sxema.ts.

2.2 Natija oynasida: sxemani sinash

Zod natija oynasida ishlaydi — sxemani jonli tekshiramiz. Ikkita ma'lumot: biri "UI'siz hujumchi" yuborgandek, biri to'g'ri:

tsx
import { z } from "zod";

const MESSAGE_SCHEMA = z.object({
  ism: z
    .string()
    .trim()
    .min(2, "Ismingizni yozing (kamida 2 harf)")
    .max(60, "Ism 60 belgidan oshmasin"),
  email: z.email("Email manzili noto'g'ri").max(120),
  mavzu: z.enum(["ish", "savol"], "Mavzuni tanlang"),
  xabar: z
    .string()
    .trim()
    .min(10, "Xabar kamida 10 belgi bo'lsin")
    .max(2000, "Xabar 2000 belgidan oshmasin"),
});

const bad = MESSAGE_SCHEMA.safeParse({
  ism: " A ",
  email: "aziz-at-example",
  mavzu: "reklama",
  xabar: "salom",
});
if (!bad.success) {
  for (const issue of bad.error.issues) {
    console.log(`${String(issue.path[0])}: ${issue.message}`);
  }
}

const good = MESSAGE_SCHEMA.safeParse({
  ism: "  Aziz  ",
  email: "aziz@example.com",
  mavzu: "savol",
  xabar: "Portfolio'ingiz juda yoqdi!",
});
console.log(good.success, good.success && good.data.ism);

Konsolda:

text
ism: Ismingizni yozing (kamida 2 harf)
email: Email manzili noto'g'ri
mavzu: Mavzuni tanlang
xabar: Xabar kamida 10 belgi bo'lsin
true Aziz

Uch narsaga qarang. " A " trim'dan keyin bitta harf — xato. mavzu: "reklama" — ro'yxatda yo'q, UI'da bunday radio yo'q, lekin POST bilan yuborish mumkin edi. To'g'ri ma'lumotda good.data.ism — "Aziz", bo'shliqlarsiz: tozalangan qiymat bilan ishlaymiz, xom FormData bilan emas.

safeParse xato tashlamaydi — natija obyektini qaytaradi (success: true/false). Server Action'da aynan shu kerak: xato — oddiy holat, istisno emas.

Natija oynasidagi Zod — 4.6.5, kanon portfolio da 4.6.7. Bu misolda farq yo'q.

3. Natija holati: useActionState

3.1 Holat turi

Formaning uch holati bor: hali yuborilmagan, xato bilan qaytgan va qabul qilingan. Kanon ularni bitta turda yozdi:

ts
export type FormaHolati =
  | { holat: "bosh" }
  | {
      holat: "xato";
      xatolar: Partial<Record<Maydon, string>>;
      umumiy?: string;
      qiymatlar: Partial<Record<Maydon, string>>;
    }
  | { holat: "yuborildi"; ism: string };

Bu — discriminated union (T-12): holat maydoni qaysi variant ekanini aytadi, TypeScript esa har variantda faqat o'z maydonlariga ruxsat beradi. holat.holat === "xato" dan keyin holat.xatolar mavjud, "yuborildi" da — yo'q.

Maydon Nega kerak
xatolar Har maydon yonida o'z xatosi
umumiy Maydonga bog'liq bo'lmagan xato ("Juda ko'p urinish")
qiymatlar Foydalanuvchi yozganlari qaytib keladi — qayta yozmaydi

qiymatlar ni unutish — eng ko'p uchraydigan xato. JS'siz formada server butun sahifani qayta chizadi; qiymatlar qaytarilmasa, xato bilan birga hamma yozilgan matn ham yo'qoladi.

3.2 Action'ning yangi imzosi

useActionState action'ni ikki argument bilan chaqiradi: oldingi holat va FormData. Kanon xabarYubor ning tekshiruv qismi:

ts
const MAYDONLAR: Maydon[] = ["ism", "email", "mavzu", "xabar"];

export async function xabarYubor(
  _oldingi: FormaHolati,
  forma: FormData,
): Promise<FormaHolati> {
  const qiymatlar = Object.fromEntries(
    MAYDONLAR.map((m) => [m, String(forma.get(m) ?? "")]),
  );
  // … asal idish va tezlik cheklovi (N-23)
  const natija = tekshir(qiymatlar);
  if (!natija.success) {
    const xatolar: Partial<Record<Maydon, string>> = {};
    for (const x of natija.error.issues) {
      const m = x.path[0] as Maydon;
      xatolar[m] ??= x.message;
    }
    return { holat: "xato", xatolar, qiymatlar };
  }
  await xabarniSaqla(natija.data);
  return { holat: "yuborildi", ism: natija.data.ism };
}

Uch detal:

  • MAYDONLAR.map(...) — FormData dan faqat kutilgan maydonlar olinadi. Hujumchi qo'shgan ortiqcha maydon (rol=admin) hech qayerga o'tmaydi.
  • xatolar[m] ??= x.message — bir maydonda bir nechta xato bo'lsa, faqat birinchisi (??=). Foydalanuvchiga bitta aniq gap yetarli.
  • tekshir — sxema.ts dagi XABAR_SXEMASI.safeParse o'rami.

3.3 Formada

Kanon AloqaFormasi.tsx (client komponent) — asosiy qismi:

tsx
"use client";

import { useActionState, useId } from "react";
import type { FormaHolati, Maydon } from "../_amal/sxema";
import { xabarYubor } from "../_amal/xabarYubor";

const BOSH: FormaHolati = { holat: "bosh" };

export function AloqaFormasi() {
  const [holat, amal] = useActionState(xabarYubor, BOSH);
  const id = useId();
  const xato = (m: Maydon) =>
    holat.holat === "xato" ? holat.xatolar[m] : undefined;
  const qiymat = (m: Maydon) =>
    holat.holat === "xato" ? holat.qiymatlar[m] : undefined;

  if (holat.holat === "yuborildi") {
    return (
      <p role="status" className="forma-natija">
        Rahmat{holat.ism ? `, ${holat.ism}` : ""}! Xabaringiz qabul
        qilindi — odatda bir kun ichida javob beraman.
      </p>
    );
  }
  // … <form action={amal} noValidate={holat.holat === "xato"}>
}

useActionState ikkita narsa qaytaradi. Birinchisi — joriy holat. Ikkinchisi — amal, formaga beriladigan o'ram. <form action={amal}> — Server Action'ning o'zi emas, o'rami, lekin JS'siz ham ishlaydi: React uni Server Action deb taniydi va yashirin maydonlarni qo'yadi (oldingi darsdagi o'lchov aynan shu formada edi).

noValidate={holat.holat === "xato"} — qiziq yechim. Birinchi yuborishda brauzer tekshiruvi ishlaydi (tez, server so'rovisiz). Server xato qaytargach esa brauzer tekshiruvi o'chadi — endi server xabarlari ko'rinadi, ikki xil xabar bir-birini to'smaydi.

3.4 Maydon va xato: accessibility

Har maydon xatosi ekran o'quvchiga ham yetishi kerak (Malika uchun). Kanondagi yordamchi:

tsx
const aria = (m: Maydon) => ({
  id: `${id}-${m}`,
  name: m,
  defaultValue: qiymat(m),
  "aria-invalid": xato(m) ? true : undefined,
  "aria-describedby": xato(m) ? `${id}-${m}-xato` : undefined,
});
Atribut Vazifasi
defaultValue Qaytgan qiymat maydonga qayta tushadi
aria-invalid Ekran o'quvchi "noto'g'ri" deydi
aria-describedby Xato matni maydon tavsifi sifatida o'qiladi
useId() Bir sahifada ikki forma bo'lsa ham id takrorlanmaydi

Formaning tepasida umumiy xabar — <p role="alert">Formada xato bor — maydonlarni tekshiring.</p>. role="alert" uni darhol o'qitadi.

Kanon o'lchovi (natijalar/23-forma.txt) — JS bilan, brauzer tekshiruvi chetlab o'tilgan holatda:

text
1) server xatolari: ["Formada xato bor — maydonlarni tekshiring.","Ismingizni yozing (kamida 2 harf)","Email manzili noto'g'ri","Xabar kamida 10 belgi bo'lsin"]
   aria-invalid: 3 | qiymat saqlandi: notogri
   axe (xato holati): 0 

Uchta maydonda aria-invalid, yozilgan email (notogri) maydonda qoldi, axe — 0 ta buzilish.

4. useFormStatus: "yuborilmoqda"

useFormStatus — react-dom dan. U ota <form> ning holatini beradi, shuning uchun alohida komponent ichida chaqiriladi (formaning o'zida emas — R-51 dagi qoida):

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

// useFormStatus — ota <form> holati; `data` — yuborilayotgan
// FormData: javob kelguncha matnni darhol ko'rsatamiz
function YuborishTugmasi() {
  const { pending, data } = useFormStatus();
  return (
    <>
      <button
        type="submit"
        aria-disabled={pending}
        disabled={pending}
      >
        {pending ? "Yuborilmoqda…" : "Yuborish"}
      </button>
      {pending && (
        <p role="status" className="ikkinchi">
          «{String(data?.get("xabar") ?? "").slice(0, 60)}…»
          yuborilmoqda
        </p>
      )}
    </>
  );
}

pending — so'rov ketyapti. data — aynan yuborilayotgan FormData. Kanon undan xabarning birinchi 60 belgisini darhol ko'rsatadi: foydalanuvchi o'z matnini javob kelmasdan ko'radi. Bu "optimistik" taassurot beradi — lekin useOptimistic siz. Nega bu muhim, «useOptimistic va JS'siz forma» bo'limida ko'rasiz.

disabled={pending} — tugmani ikki marta bosib, xabarni ikki marta yuborishning oldini oladi.

4.1 Natija oynasida: to'liq forma

Kanon formasining client qismini natija oynasida yig'amiz. Server Action o'rnida — 300 ms kutib, o'sha Zod sxemasi bilan tekshiradigan async funksiya. Hech narsa yozmasdan «Yuborish» ni bosamiz:

tsx
import { useActionState } from "react";
import { useFormStatus } from "react-dom";
import { z } from "zod";

const SCHEMA = z.object({
  ism: z.string().trim().min(2, "Ismingizni yozing (kamida 2 harf)"),
  xabar: z.string().trim().min(10, "Xabar kamida 10 belgi bo'lsin"),
});
type Field = keyof z.infer<typeof SCHEMA>;
type State =
  | { holat: "bosh" }
  | { holat: "xato"; xatolar: Partial<Record<Field, string>> }
  | { holat: "yuborildi"; ism: string };

// Server Action o'rnida (Next.js'da — "use server" fayl)
async function sendMessage(_prev: State, form: FormData) {
  await new Promise((r) => setTimeout(r, 300));
  const result = SCHEMA.safeParse(Object.fromEntries(form));
  if (!result.success) {
    const xatolar: Partial<Record<Field, string>> = {};
    for (const i of result.error.issues) {
      xatolar[i.path[0] as Field] ??= i.message;
    }
    return { holat: "xato", xatolar } satisfies State;
  }
  return { holat: "yuborildi", ism: result.data.ism } satisfies State;
}

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

export default function App() {
  const [state, action] = useActionState<State, FormData>(
    sendMessage,
    { holat: "bosh" },
  );
  if (state.holat === "yuborildi") {
    return <p role="status">Rahmat, {state.ism}!</p>;
  }
  const err = (f: Field) =>
    state.holat === "xato" ? state.xatolar[f] : undefined;
  return (
    <form action={action} noValidate>
      <label>
        Ismingiz <input name="ism" aria-invalid={!!err("ism")} />
      </label>
      {err("ism") && <p>{err("ism")}</p>}
      <label>
        Xabar <textarea name="xabar" aria-invalid={!!err("xabar")} />
      </label>
      {err("xabar") && <p>{err("xabar")}</p>}
      <SubmitButton />
    </form>
  );
}

Ekranda:

text
Ismingiz
Ismingizni yozing (kamida 2 harf)
Xabar
Xabar kamida 10 belgi bo'lsin
Yuborish

ekran("Yuborilmoqda…") qadami bosishdan keyingi lahzani tekshiradi: tugma matni o'zgargan. 300 ms dan keyin server "xatolari" maydonlar ostida. O'zingiz sinang: ism va 10 belgidan uzun xabar yozib yuboring — "Rahmat, …!" chiqadi.

satisfies State — qaytgan obyekt turini tekshiradi, lekin uni kengaytirmaydi (satisfies). Bu blokda forma qiymatlarini qaytarmadik — kanon qaytaradi; buni mashqda qo'shasiz.

Tekshirib ko'ring: SubmitButton ni olib tashlab, useFormStatus ni to'g'ridan App ichida chaqirsak, pending nima bo'ladi?

Javob

Doim false. useFormStatus o'zi joylashgan komponentning ota formasini qidiradi. App formani chizadi, lekin formaning ichida emas — demak ota forma yo'q. Shuning uchun tugma alohida komponentga chiqariladi.

5. useOptimistic va JS'siz forma

5.1 Optimistik UI nima uchun

Optimistik yangilash — server javobini kutmasdan, "muvaffaqiyatli bo'ladi" deb UI'ni darhol o'zgartirish. Telegram'da xabar yuborsangiz, u darhol ro'yxatda soat belgisi bilan paydo bo'ladi — server tasdiqlaganda belgi o'zgaradi. 18-qismda useOptimistic bilan ko'rgansiz:

tsx
import { useOptimistic, useState } from "react";

type Comment = { text: string; sending?: boolean };

export default function App() {
  const [comments, setComments] = useState<Comment[]>([
    { text: "Rahmat, foydali bo'ldi" },
  ]);
  const [shown, addOptimistic] = useOptimistic(
    comments,
    (list: Comment[], text: string) => [
      ...list,
      { text, sending: true },
    ],
  );

  async function send(form: FormData) {
    const text = String(form.get("izoh") ?? "");
    addOptimistic(text); // darhol ko'rinadi
    await new Promise((r) => setTimeout(r, 300)); // "server"
    setComments((c) => [...c, { text }]);
  }

  return (
    <>
      <ul>
        {shown.map((c, i) => (
          <li key={i}>
            {c.text}
            {c.sending && " (yuborilmoqda)"}
          </li>
        ))}
      </ul>
      <form action={send}>
        <label>
          Izoh <input name="izoh" />
        </label>
        <button type="submit">Yuborish</button>
      </form>
    </>
  );
}

Ekranda:

text
Rahmat, foydali bo'ldi
Zo'r maqola!
Izoh Yuborish

ekran(...) qadami oraliq lahzani tekshiradi: izoh "(yuborilmoqda)" belgisi bilan darhol ro'yxatda. 300 ms dan keyin optimistik qiymat haqiqiy holat bilan almashadi — belgi yo'qoladi.

5.2 Biz o'lchadik: optimistik o'ram JS'siz formani buzadi

Kanon useOptimistic ni olmadi va sababini yozib qo'ydi: "formani client funksiyaga o'rash JS'siz ishlashni buzadi". Biz buni o'lchab ko'rdik. Kanon AloqaFormasi ga useOptimistic qo'shdik — odatiy usul bilan, action'ni client funksiyaga o'rab:

tsx
const [yuborilmoqda, optimistikBelgila] = useOptimistic<
  string | null,
  string
>(null, (_, x) => x);

async function yubor(fd: FormData) {
  optimistikBelgila(String(fd.get("xabar") ?? ""));
  amal(fd); // useActionState o'rami
}

// …
<form action={yubor}>

Keyin next build + next start va «Server Actions I» dagi o'sha o'lchov skripti (headless Chrome, JS yoqilgan va o'chiq):

text
JS yoqilgan: form action=javascript:throw new Error('React form unexpectedly submitted.') | yashirin: (yo'q)
  POST /aloqa content-type=multipart/form-data next-action=60fd8924…(42 belgi)
  URL: /aloqa | natija: Rahmat, Aziz! Xabaringiz qabul qilindi — odatda bir kun ichida javob b
JS o'chiq: form action=javascript:throw new Error('React form unexpectedly submitted.') | yashirin: (yo'q)
  URL: /aloqa | natija: Aloqa

Nima o'zgardi:

Kanon (action={amal}) Optimistik (action={yubor})
HTML'dagi action bo'sh (joriy sahifa) javascript:throw new Error(…)
Yashirin $ACTION_… maydonlar 4 ta yo'q
JS yoqilgan Ishlaydi Ishlaydi
JS o'chiq POST, "Rahmat, Malika!" POST yo'q, sahifa joyida

E'tibor bering: formani buzgan narsa useOptimistic ning o'zi emas, action ga berilgan client funksiya (yubor). onSubmit yoki RHF o'rami ham xuddi shunday ta'sir qiladi (pastda «React Hook Form» bo'limida). Bu kanon o'lchovi emas — kanon qarorini tekshirish uchun o'zimiz ishga tushirdik.

yubor — brauzerdagi oddiy funksiya. Server uni HTML'ga yoza olmaydi, shuning uchun React action ga "bu forma JS'siz yuborilmasligi kerak" degan himoya satrini qo'yadi. JS o'chiq foydalanuvchi «Yuborish» ni bosadi — va hech narsa bo'lmaydi. Xabar ketmadi, xato ham ko'rinmadi. Bu eng yomon natija: foydalanuvchi "yubordim" deb o'ylaydi.

Kanon nima qildi? Optimistik taassurotni useFormStatus().data bilan berdi — yuborilayotgan matn tugma ostida darhol ko'rinadi. Forma esa to'g'ridan action={amal} — progressive enhancement saqlandi. Aloqa formasida ro'yxat yo'q, "darhol paydo bo'ladigan element" kerak emas — shuning uchun bu yetarli.

5.3 Qachon useOptimistic to'g'ri

Vaziyat Yechim
Ro'yxatga element qo'shish (izoh, vazifa, chat) useOptimistic — foydasi katta
"Yoqdi" tugmasi, belgilash useOptimistic + startTransition (forma emas)
Aloqa, ro'yxatdan o'tish, to'lov useFormStatus — JS'siz ishlash muhimroq

Ro'yxatli formada ham JS'siz ishlashni saqlash mumkin: optimistik qism alohida tugma onClick da, forma esa baribir Server Action'ga. Murakkablik oshadi — har holatda narxni o'lchang.

6. React Hook Form va next-safe-action

6.1 RHF bilan

19-qismda React Hook Form'ni o'rgangansiz: maydon darajasidagi jonli tekshiruv, zodResolver, murakkab formalar. Server Action bilan birga ishlatish mumkin:

tsx
"use client";
const { register, handleSubmit } = useForm<Xabar>({
  resolver: zodResolver(XABAR_SXEMASI), // o'sha sxema!
});
const [holat, amal] = useActionState(xabarYubor, BOSH);
const [, startTransition] = useTransition();

const yubor = handleSubmit((qiymatlar) => {
  const fd = new FormData();
  for (const [k, v] of Object.entries(qiymatlar)) fd.set(k, v);
  startTransition(() => amal(fd));
});

return <form onSubmit={yubor}>{/* … */}</form>;

Afzalligi — bitta sxema ikki joyda: brauzerda (darhol, yozish paytida) va serverda (himoya). Narxi — onSubmit va RHF'ning JS'i: forma yana JS'siz ishlamaydi, va brauzer to'plami RHF + Zod qadar kattalashadi. Kanon formasi 4 maydonli — RHF kerak emas. 10+ maydonli, bosqichli, dinamik maydonli formada esa RHF foydali.

6.2 next-safe-action

Har action'da bir xil narsa takrorlanadi. Zod bilan tekshirish, xatolarni maydonlarga taqsimlash, auth tekshiruvi — har safar qaytadan. next-safe-action — shu naqshni kutubxonaga aylantirgan paket: sxemani beradi, action tanasi esa allaqachon tekshirilgan ma'lumotni oladi, xatolar standart shaklda qaytadi. Kanon uni o'rnatmadi va o'lchamadi — bitta action uchun qo'lda yozilgan 15 qator yetarli. Ko'p action'li loyihada (admin panel) ko'rib chiqishga arziydi. Versiya va API — uning rasmiy hujjatidan.

7. Portfolio qadami: validatsiya va holat

Holat 23 ning ikkinchi qismi. Commit — Server Actions xavfsizligi da. Hali commit qilmang.

  1. npm install --save-exact zod@4.6.7.
  2. aloqa/_amal/sxema.ts — sxema, Xabar, Maydon, FormaHolati, tekshir.
  3. xabarYubor — yangi imzo (_oldingi, forma), qiymatlarni yig'ish, tekshir, xatolar yoki "yuborildi".
  4. aloqa/_komponentlar/AloqaFormasi.tsx — useActionState, useId, aria-*, YuborishTugmasi (useFormStatus), mavzu radio'lari.
  5. aloqa/page.tsx — <AloqaFormasi />.

Tekshiruv: bo'sh forma bilan yuboring (brauzer tekshiruvi to'xtatadi), keyin DevTools'da novalidate qo'shib yoki curl bilan yuboring — server xatolari maydonlar ostida. Kanon o'lchovi: Zod faqat serverda — brauzer JS'i o'zgarmadi, forma komponenti +1.7 KB; axe 0.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
formData o'rnida holat obyekti keldi useActionState bilan imzo eski (forma) (_oldingi, forma)
Xatodan keyin yozilganlar yo'qoldi qiymatlar qaytarilmagan qiymatlar + defaultValue
pending doim false useFormStatus forma komponentining o'zida Alohida bola komponent
Bir maydonda 3 ta xato chiqdi Hamma issues ko'rsatilgan ??= bilan birinchisi
Hujumchi ortiqcha maydon yubordi va saqlandi Object.fromEntries(forma) to'g'ridan Kutilgan maydonlar ro'yxati + Zod
JS'siz forma jim, hech narsa ketmaydi action client funksiyaga o'ralgan action={amal}; optimistikni useFormStatus bilan
Ekran o'quvchi xatoni o'qimaydi aria-describedby yo'q Xato id si bilan bog'lang

9. Mashqlar

Mashqlar kurs/mashqlar/23/22-validatsiya/ da. Jonli yechimlarni «Ishga tushir» bilan sinang.

1-mashq (oson): qaysi vosita?

  • Action natijasini (xato/muvaffaqiyat) komponentda ko'rsatish:
  • Tugmada "Yuborilmoqda…":
  • Izoh javobdan oldin ro'yxatda paydo bo'lsin:
  • Serverda ma'lumotni tekshirish:
Yechim
  • useActionState — action qaytargan holat.
  • useFormStatus — ota formaning pending va data si.
  • useOptimistic — lekin JS'siz forma narxini unutmang.
  • Zod (safeParse) — brauzer tekshiruviga ishonmaymiz.

2-mashq (o'rta): qiymatlarni qaytarish

«To'liq forma» blokini kengaytiring: xato holatida action qiymatlar ni ham qaytarsin va inputlar defaultValue={…} bilan ularni ko'rsatsin. Faqat ism yozib («A») yuborganda ekranda ikkala xato chiqsin. Qiymat ekranda ko'rinmaydi (input qiymati Ekranda: ga kirmaydi), shuning uchun konsolga qaytdi: A deb yozing.

Yechim
tsx
import { useActionState } from "react";
import { z } from "zod";

const SCHEMA = z.object({
  ism: z.string().trim().min(2, "Ismingizni yozing (kamida 2 harf)"),
  xabar: z.string().trim().min(10, "Xabar kamida 10 belgi bo'lsin"),
});
type Field = keyof z.infer<typeof SCHEMA>;
type Values = Partial<Record<Field, string>>;
type State =
  | { holat: "bosh" }
  | { holat: "xato"; xatolar: Values; qiymatlar: Values }
  | { holat: "yuborildi"; ism: string };

async function sendMessage(_prev: State, form: FormData) {
  await new Promise((r) => setTimeout(r, 300));
  const qiymatlar: Values = {
    ism: String(form.get("ism") ?? ""),
    xabar: String(form.get("xabar") ?? ""),
  };
  const result = SCHEMA.safeParse(qiymatlar);
  if (!result.success) {
    const xatolar: Values = {};
    for (const i of result.error.issues) {
      xatolar[i.path[0] as Field] ??= i.message;
    }
    console.log("qaytdi:", qiymatlar.ism);
    return { holat: "xato", xatolar, qiymatlar } satisfies State;
  }
  return { holat: "yuborildi", ism: result.data.ism } satisfies State;
}

export default function App() {
  const [state, action] = useActionState<State, FormData>(
    sendMessage,
    { holat: "bosh" },
  );
  if (state.holat === "yuborildi") return <p>Rahmat, {state.ism}!</p>;
  const err = (f: Field) =>
    state.holat === "xato" ? state.xatolar[f] : undefined;
  const val = (f: Field) =>
    state.holat === "xato" ? state.qiymatlar[f] : undefined;
  return (
    <form action={action} noValidate>
      <label>
        Ismingiz <input name="ism" defaultValue={val("ism")} />
      </label>
      {err("ism") && <p>{err("ism")}</p>}
      <label>
        Xabar <textarea name="xabar" defaultValue={val("xabar")} />
      </label>
      {err("xabar") && <p>{err("xabar")}</p>}
      <button type="submit">Yuborish</button>
    </form>
  );
}

Konsolda:

text
qaytdi: A

Ekranda:

text
Ismingiz
Ismingizni yozing (kamida 2 harf)
Xabar
Xabar kamida 10 belgi bo'lsin
Yuborish

React action'dan keyin formani tozalaydi, lekin defaultValue yangi holatdan "A" ni qaytaradi — foydalanuvchi o'z matnini ko'radi. JS'siz formada ham xuddi shunday: server yangi HTML'ni value="A" bilan chizadi.

3-mashq (qiyin): xatolarni taqsimlash

Zod issues ro'yxatini maydon xatolariga aylantiradigan umumiy funksiya yozing: toFieldErrors(issues) → { maydon: birinchi xabar }. Ichma-ich maydon (path: ["manzil", "shahar"]) uchun kalit "manzil.shahar" bo'lsin; yo'lsiz xato (path: []) — "_umumiy" kalitiga. Uchta issue bilan sinang.

Yechim
js
function toFieldErrors(issues) {
  const errors = {};
  for (const issue of issues) {
    const key = issue.path.length ? issue.path.join(".") : "_umumiy";
    errors[key] ??= issue.message;
  }
  return errors;
}

console.log(
  toFieldErrors([
    { path: ["ism"], message: "Ismingizni yozing" },
    { path: ["ism"], message: "Ism 60 belgidan oshmasin" },
    { path: ["manzil", "shahar"], message: "Shaharni tanlang" },
    { path: [], message: "Parollar mos emas" },
  ]),
);

Konsolda:

text
{
  ism: 'Ismingizni yozing',
  'manzil.shahar': 'Shaharni tanlang',
  _umumiy: 'Parollar mos emas'
}

??= ikkinchi ism xatosini tashlab yubordi. Yo'lsiz xato — sxema darajasidagi refine dan keladi (masalan, ikki parol solishtiruvi, Zod chuqurroq). Zod 4 da shunga o'xshash tayyor yordamchi ham bor (z.flattenError), lekin u har maydon uchun massiv qaytaradi — o'z funksiyangiz UI'ga qulayroq shakl beradi.

4-mashq: Portfolio qadami

«Portfolio qadami» bo'limidagi besh qadamni bajaring. DevTools'da <form> ga novalidate atributini qo'lda qo'shib, bo'sh forma yuboring — server xatolari va aria-invalid ni tekshiring. Keyin to'g'ri ma'lumot bilan yuboring: "Rahmat, …!". Commit hali yo'q.

Yechim

Namuna — «Kanon sxemasi», «Action'ning yangi imzosi», «Formada» va «useFormStatus» bo'limlaridagi kod. Kutilgan natija kanon o'lchovidagidek: umumiy xabar + 3 maydon xatosi, aria-invalid 3 ta, yozilgan qiymat saqlangan. Sekin tarmoqni (DevTools → Network → "Slow 4G") yoqsangiz, tugma ostida «…» yuborilmoqda yozuvini ko'rasiz.

10. Real ishda

  • Har Next.js formasi shu uchlik bilan yoziladi: Zod (server), useActionState (natija), useFormStatus (kutish). Kod review'da birinchi savol: "server tekshiryaptimi?".
  • Bitta sxema — ikki joyda (brauzer va server) — katta jamoalarda standart; tur ham sxemadan chiqadi.
  • Optimistik UI — chat, izohlar, vazifalar, "yoqdi" tugmalari. Lekin to'lov va buyurtmada — hech qachon (pul ketmagan bo'lsa "to'landi" deb ko'rsatish xavfli).
  • Intervyuda: "Nega klient validatsiyasi yetarli emas?", "useActionState qanday ishlaydi?", "Progressive enhancement nima?".

Xulosa

  • useActionState(action, boshlangich) action natijasini holatga aylantiradi; action imzosi — (oldingiHolat, formData).
  • Zod safeParse serverda tekshiradi; xatolar maydon bo'yicha, birinchisi bilan; qiymatlar qaytariladi.
  • Holat turi — discriminated union: bosh | xato | yuborildi.
  • useFormStatus — alohida komponentda: pending va yuborilayotgan data.
  • useOptimistic formani client funksiyaga o'rasa, JS'siz forma jim ishlamay qoladi — biz o'lchadik, kanon useFormStatus().data ni tanladi.
  • RHF + Server Action — bitta sxema ikki joyda, lekin JS'siz ishlash yo'qoladi.

Keyingi dars: Server Actions xavfsizligi — action'ni UI'siz chaqiramiz, CSRF, asal idish va tezlik cheklovini haqiqiy so'rovlar bilan sinaymiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Next.js Server Actions: useActionState, Zod validatsiya va optimistik UI — IlmHamroh