useActionState: مدیریت نتیجه و وضعیت یک Action

در درس قبل دیدیم Action چیست و React وضعیت isPending آن را خودکار مدیریت می‌کند. اما نتیجه Action (پیام موفقیت، پیام خطا، داده برگشتی) همچنان باید با یک useState جداگانه نگه‌داری می‌شد. useActionState این دو را در یک Hook کنار هم می‌گذارد: Action را اجرا می‌کند، نتیجه آن را به‌عنوان State نگه می‌دارد و وضعیت در حال اجرا بودن را هم برمی‌گرداند.

نحو پایه

jsx
import { useActionState } from "react";

const [state, formAction, isPending] = useActionState(action, initialState);

این Hook دو آرگومان ضروری و سه مقدار برگشتی دارد:

  • action: تابعی که کار اصلی را انجام می‌دهد و می‌تواند async باشد.
  • initialState: مقدار اولیه State، پیش از اولین اجرای Action.
  • state: آخرین مقداری که Action برگردانده است (یا همان مقدار اولیه، تا اولین اجرا).
  • formAction: نسخه آماده‌شده از Action که می‌توان آن را به ویژگی action یک فرم داد.
  • isPending: مقدار Boolean که تا پایان کامل Action برابر true است.

امضای تابع Action

تفاوت مهم این تابع با Actionهای درس قبل این است که آرگومان اول آن، State قبلی است و آرگومان دوم، FormData فرم (وقتی با فرم استفاده شود). مقداری که تابع برمی‌گرداند، State جدید می‌شود:

jsx
async function signupAction(prevState, formData) {
  const email = formData.get("email");

  if (!email.includes("@")) {
    return { status: "error", message: "ایمیل معتبر نیست" };
  }

  await sendSignup(email);
  return { status: "success", message: "ثبت‌نام انجام شد" };
}

استفاده در کامپوننت

jsx
function Signup() {
  const [state, formAction, isPending] = useActionState(signupAction, {
    status: "idle",
    message: "",
  });

  return (
    <form action={formAction}>
      <input name="email" />
      <button type="submit" disabled={isPending}>
        {isPending ? "در حال ارسال..." : "ثبت‌نام"}
      </button>
      {state.message && <p>{state.message}</p>}
    </form>
  );
}

در مقایسه با نسخه درس قبل، دیگر نه useState برای خطا لازم است، نه useTransition برای isPending، و نه onSubmit و preventDefault که در فصل چهارم می‌نوشتیم.

خطا را برگردانید، نه پرتاب کنید

برای خطاهای قابل‌انتظار (مثل ایمیل نامعتبر)، الگوی توصیه‌شده این است که خطا را به‌عنوان بخشی از State برگردانید، نه اینکه throw کنید. State برگشتی همان‌جا نمایش داده می‌شود و فرم سر جایش می‌ماند. اگر داخل Action خطایی پرتاب شود، طبق چیزی که در درس قبل و فصل هفدهم دیدیم، به نزدیک‌ترین Error Boundary می‌رسد و جای فرم را با Fallback آن عوض می‌کند؛ این مسیر برای خطاهای غیرمنتظره مناسب است.

Actionها پشت‌سرهم و به‌ترتیب اجرا می‌شوند

اگر کاربر قبل از پایان یک Action دوباره فرم را ارسال کند، اجرای دوم کنار گذاشته نمی‌شود و هم‌زمان هم اجرا نمی‌شود؛ در صف قرار می‌گیرد و بعد از تمام شدن اولی شروع می‌شود. آرگومان prevState در اجرای دوم، همان خروجی اجرای اول است. به همین دلیل غیرفعال کردن دکمه با isPending معمولاً کار درستی است، ولی حتی اگر فراموش شود، State دچار تداخل نمی‌شود.

فیلدهای فرم پس از Action بازنشانی می‌شوند

همان‌طور که در درس قبل گفتیم، React بعد از موفقیت یک Form Action، فیلدهای Uncontrolled فرم را بازنشانی می‌کند. این برای فرم موفق خوب است، ولی اگر Action با خطا تمام شود، کاربر انتظار ندارد آنچه نوشته پاک شود. راه‌حل رایج، برگرداندن مقدار فیلد داخل State و دادن آن به defaultValue است:

jsx
return { status: "error", message: "ایمیل معتبر نیست", email };

// در JSX
<input name="email" defaultValue={state.email} />

نام قدیمی این Hook

اگر در کدهای قدیمی‌تر یا آموزش‌های ۲۰۲۴ به useFormState از بسته react-dom رسیدید، همان Hook با نام قبلی است. نام جدید و پیشنهادی useActionState از بسته react است، و Hook قدیمی منسوخ (Deprecated) شده است.

آرگومان سوم: permalink

این Hook یک آرگومان سوم اختیاری به نام permalink هم دارد. آن فقط وقتی معنا پیدا می‌کند که Action یک Server Function باشد و بخواهیم فرم حتی پیش از بارگذاری جاوااسکریپت هم کار کند. این موضوع به معماری Server/Client وابسته است و در فصل بیست‌وچهارم دیده می‌شود.

مثال قابل اجرا

در این مثال، اگر ایمیل بدون علامت @ وارد شود، پیام خطا همراه با مقدار قبلی فیلد برمی‌گردد. اگر معتبر باشد، پس از یک تأخیر ساختگی پیام موفقیت می‌آید و فیلد خالی می‌شود.

فرم ثبت‌نام با useActionState
import { useActionState } from "react";

function fakeSignup(email) {
  return new Promise((resolve) => setTimeout(resolve, 1000));
}

async function signupAction(prevState, formData) {
  const email = formData.get("email");

  if (!email.includes("@")) {
    return { status: "error", message: "ایمیل معتبر نیست", email };
  }

  await fakeSignup(email);
  return { status: "success", message: "ثبت‌نام انجام شد: " + email, email: "" };
}

function App() {
  const [state, formAction, isPending] = useActionState(signupAction, {
    status: "idle",
    message: "",
    email: "",
  });

  return (
    <form action={formAction}>
      <input name="email" defaultValue={state.email} placeholder="ایمیل" />
      <button type="submit" disabled={isPending}>
        {isPending ? "در حال ارسال..." : "ثبت‌نام"}
      </button>
      {state.message && <p>{state.message}</p>}
    </form>
  );
}
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

useActionState یک Action را به‌همراه State نتیجه و isPending در یک Hook جمع می‌کند. تابع Action، State قبلی و FormData را می‌گیرد و State جدید را برمی‌گرداند. خطاهای قابل‌انتظار بهتر است به‌عنوان State برگردانده شوند و خطاهای غیرمنتظره به Error Boundary بروند. Actionها در صف و به‌ترتیب اجرا می‌شوند و فیلدهای Uncontrolled پس از Action بازنشانی می‌شوند. نام قدیمی این Hook useFormState است و منسوخ شده. در درس بعدی، به useOptimistic می‌پردازیم.