Actions: به‌روزرسانی‌های غیرهمزمان به سبک React مدرن

تا اینجا، هر بار که می‌خواستیم یک عملیات غیرهمزمان (مثل ارسال فرم به سرور) را مدیریت کنیم، خودمان چند State جداگانه برای loading و error نگه داشتیم و در هر مرحله دستی آن‌ها را عوض کردیم؛ همان الگوی فصل چهاردهم. React ۱۹ برای این کار یک قرارداد رسمی ارائه می‌دهد که به آن Action می‌گویند. این درس مفهوم کلی Action را می‌سازد، و سه درس بعدی (useActionState، useOptimistic و use) ابزارهای مرتبط با آن را جداگانه بررسی می‌کنند.

Action چیست

Action یک API یا Hook جدید با آن نام نیست و چیزی به نام Action برای import وجود ندارد. در React مدرن، به هر تابعی که داخل یک Transition اجرا می‌شود و می‌تواند غیرهمزمان (async) باشد، Action گفته می‌شود. به بیان ساده‌تر: Action یعنی startTransition که تابع async می‌پذیرد، به‌علاوه چند امکان خودکار که روی آن سوار شده است.

یک تصحیح نسبت به درس‌های قبل

در درس ۲۲-۴ گفتیم تابعی که به startTransition داده می‌شود باید هم‌زمان باشد. این محدودیت مربوط به نسخه‌های قدیمی‌تر React بود و از React ۱۹ برداشته شده است؛ اکنون می‌توان یک تابع async به آن داد. این دقیقاً همان قابلیتی است که Action بر آن بنا شده.

مشکل: مدیریت دستی وضعیت‌های یک عملیات

jsx
function UpdateName() {
  const [name, setName] = useState("");
  const [error, setError] = useState(null);
  const [isPending, setIsPending] = useState(false);

  async function handleSubmit() {
    setIsPending(true);
    const err = await updateName(name);
    setIsPending(false);
    if (err) {
      setError(err);
    }
  }

  // ...
}

برای یک عملیات ساده، سه State و ترتیب دقیقی از فراخوانی‌ها لازم است. اگر بین این مراحل چیزی فراموش شود (مثلاً خاموش نکردن isPending در مسیر خطا)، رابط کاربری در وضعیت نادرست گیر می‌کند.

نسخه Action

jsx
function UpdateName() {
  const [name, setName] = useState("");
  const [error, setError] = useState(null);
  const [isPending, startTransition] = useTransition();

  function handleSubmit() {
    startTransition(async () => {
      const err = await updateName(name);
      if (err) {
        setError(err);
      }
    });
  }

  // ...
}

حالا isPending را خود React مدیریت می‌کند: از لحظه شروع Action، تا پایان کامل آن (شامل همه awaitها) برابر true است و بعد خودکار false می‌شود. دیگر لازم نیست دستی روشن و خاموشش کنیم.

یک نکته مهم درباره کد بعد از await

به‌روزرسانی‌های State که بعد از یک await انجام می‌شوند، به‌طور خودکار Transition حساب نمی‌شوند؛ مثل یک به‌روزرسانی معمولی و فوری اجرا می‌شوند. اگر بخواهید آن‌ها هم Transition باشند، باید دوباره داخل یک startTransition جداگانه قرار بگیرند. این محدودیت فعلی است و باید هنگام استفاده از Actionهای پیچیده به آن توجه کرد.

خطاها: همان مسیر Error Boundary

اگر داخل یک Action خطایی پرتاب شود، React آن را به نزدیک‌ترین Error Boundary می‌رساند؛ همان مکانیزمی که در فصل هفدهم دیدیم. بنابراین اگر نمی‌خواهید خطا را در همان کامپوننت با یک State محلی نشان دهید، می‌توانید آن را پرتاب کنید و به یک Error Boundary بیرونی بسپارید.

Form Actions: جایگزین onSubmit

Actions با فرم‌ها هم یکپارچه شده‌اند. به‌جای onSubmit و e.preventDefault() که در فصل چهارم دیدیم، می‌توان یک تابع را به ویژگی action خود تگ form داد:

jsx
function Signup() {
  async function signup(formData) {
    const email = formData.get("email");
    await sendSignup(email);
  }

  return (
    <form action={signup}>
      <input name="email" />
      <button type="submit">ثبت‌نام</button>
    </form>
  );
}

چند نکته درباره این الگو:

  • تابع، یک آبجکت FormData می‌گیرد که مقدار هر فیلد را بر اساس ویژگی name آن می‌دهد. این همان Uncontrolled Components فصل چهارم است؛ فیلدها لازم نیست State داشته باشند.
  • React خودش این تابع را داخل یک Transition اجرا می‌کند، پس نیازی به startTransition دستی نیست.
  • پس از موفقیت Action، React فیلدهای Uncontrolled فرم را خودکار بازنشانی می‌کند.

برای اینکه کامپوننتی که داخل همان فرم است (مثلاً یک دکمه جداگانه) بداند Action در حال اجراست، بدون اینکه isPending را از طریق Props به آن برسانیم، Hook ای به نام useFormStatus از بسته react-dom وجود دارد. این Hook فقط در کامپوننتی که داخل یک form رندر می‌شود کار می‌کند.

Action برای نوشتن داده است، نه خواندن آن

یک مرز مهم: Actionها برای تغییر دادن داده (ارسال فرم، به‌روزرسانی، حذف) طراحی شده‌اند. برای خواندن داده‌ای که قرار است نمایش داده شود، همچنان همان ابزارهای فصل چهاردهم و Suspense (فصل بیست‌ویکم) درست‌ترند. Actionها جایگزین آن‌ها نیستند.

ارتباط با فصل‌های بعد

Actionها می‌توانند روی سرور هم اجرا شوند (Server Functions)، که به وجود یک Framework با پشتیبانی Server Components بستگی دارد و در فصل بیست‌وچهارم بررسی می‌شود. در این درس فقط Actionهای سمت کلاینت را دیدیم.

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

در این مثال، دکمه ذخیره یک Action غیرهمزمان اجرا می‌کند. تا پایان آن، دکمه غیرفعال و متن آن عوض می‌شود. فیلد را خالی بگذارید و ذخیره کنید تا مسیر خطا را هم ببینید.

یک Action غیرهمزمان با useTransition
import { useState, useTransition } from "react";

function fakeSave(name) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (name.trim() === "") reject(new Error("نام نمی‌تواند خالی باشد"));
      else resolve(name);
    }, 1000);
  });
}

function App() {
  const [name, setName] = useState("");
  const [savedName, setSavedName] = useState("هنوز ذخیره نشده");
  const [error, setError] = useState(null);
  const [isPending, startTransition] = useTransition();

  function handleSave() {
    startTransition(async () => {
      setError(null);
      try {
        const result = await fakeSave(name);
        startTransition(() => {
          setSavedName(result);
        });
      } catch (e) {
        startTransition(() => {
          setError(e.message);
        });
      }
    });
  }

  return (
    <div>
      <input value={name} onChange={(e) => setName(e.target.value)} placeholder="نام" />
      <button onClick={handleSave} disabled={isPending}>
        {isPending ? "در حال ذخیره..." : "ذخیره"}
      </button>
      {error && <p>خطا: {error}</p>}
      <p>مقدار ذخیره‌شده: {savedName}</p>
    </div>
  );
}
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

Action یعنی تابعی (معمولاً async) که داخل یک Transition اجرا می‌شود. React وضعیت isPending را از شروع تا پایان کامل آن خودکار مدیریت می‌کند، خطاهای پرتاب‌شده را به Error Boundary می‌رساند، و با فرم‌ها از طریق ویژگی action یکپارچه است. به‌روزرسانی‌های State بعد از await خودکار Transition نیستند، و Actionها برای نوشتن داده‌اند، نه خواندن آن. در درس بعدی، useActionState را می‌بینیم که مدیریت نتیجه و وضعیت یک Action را ساده‌تر می‌کند.