useTransition: نسخه کامل‌تر با وضعیت در حال انتظار

در درس قبل با startTransition آشنا شدیم و دیدیم محدودیت اصلی آن این است که هیچ اطلاعی از «آیا Transition هنوز در حال اجراست» نمی‌دهد. در این درس، به useTransition می‌پردازیم: نسخه Hook همین قابلیت، که دقیقاً همین اطلاعات را هم در اختیار می‌گذارد.

نحو پایه

jsx
import { useTransition } from "react";

function ProfilePage() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState("posts");

  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }

  return (
    <div>
      <button onClick={() => selectTab("posts")}>پست‌ها</button>
      <button onClick={() => selectTab("about")}>درباره</button>

      {isPending && <p>در حال به‌روزرسانی...</p>}
      {tab === "posts" ? <HeavyPostsList /> : <AboutTab />}
    </div>
  );
}

useTransition، یک آرایه دو‌عضوی برمی‌گرداند: isPending، مقداری Boolean که تا پایان Transition برابر true است؛ و startTransition، تابعی با همان رفتار نسخه مستقلی که در درس قبل دیدیم، اما این‌بار مخصوص همین کامپوننت.

چرا isPending، شبیه Loading فصل چهاردهم نیست

ممکن است isPending در نگاه اول شبیه همان State لودینگ دستی فصل چهاردهم به نظر برسد، اما تفاوت مهمی دارد: در فصل چهاردهم، loading مربوط به یک عملیات غیرهمزمان بیرونی (مثل یک درخواست شبکه) بود. اینجا، isPending مربوط به خود فرایند Render شدن یک به‌روزرسانی داخلی React است؛ هیچ شبکه یا Promise ای در کار نیست، فقط یک محاسبه ممکن است زمان‌بر باشد.

استفاده از isPending برای نشانگر ملایم، نه قفل کامل

یک الگوی رایج، استفاده از isPending برای کم‌رنگ کردن محتوای قبلی، به‌جای جایگزینی کامل آن با یک Spinner:

jsx
<div style={{ opacity: isPending ? 0.6 : 1 }}>
  {tab === "posts" ? <HeavyPostsList /> : <AboutTab />}
</div>

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

useTransition و Suspense: تکمیل‌کننده هم

همان‌طور که در فصل بیست‌ویکم اشاره شد، اگر یک به‌روزرسانی، کامپوننتی را که از قبل روی صفحه بوده، دوباره وادار به Suspend شدن کند، رفتار پیش‌فرض Suspense، پرش فوری به fallback است. اگر همین به‌روزرسانی، داخل startTransition (از همین Hook) انجام شود، React می‌تواند محتوای قبلی را نگه دارد و به‌جای fallback کامل، همان isPending را true کند؛ این دقیقاً همان هماهنگی بین این دو ابزار است.

چه زمانی useTransition مناسب‌تر از startTransition مستقل است

اگر فقط نیاز دارید یک به‌روزرسانی را کم‌اولویت علامت بزنید و هیچ نشانه بصری از وضعیت pending لازم ندارید، startTransition مستقل کافی است. اما در اکثر رابط‌های کاربری واقعی، نشان دادن یک نشانه بصری (مثل کم‌رنگ شدن محتوا یا یک نوار پیشرفت کوچک) برای تجربه کاربری بهتر لازم است؛ در آن صورت، useTransition انتخاب طبیعی‌تری است.

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

تغییر تب با نشانگر pending ملایم
import { useState, useTransition } from "react";

function HeavyList({ label }) {
  const items = Array.from({ length: 10000 }, (_, i) => `${label} ${i}`);
  return (
    <ul>
      {items.slice(0, 15).map((item) => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  );
}

function App() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState("posts");

  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }

  return (
    <div>
      <button onClick={() => selectTab("posts")}>پست‌ها</button>
      <button onClick={() => selectTab("about")}>درباره</button>
      <div style={{ opacity: isPending ? 0.5 : 1 }}>
        <HeavyList label={tab === "posts" ? "پست" : "اطلاعات"} />
      </div>
    </div>
  );
}
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

useTransition، همان قابلیت startTransition را، همراه با یک مقدار isPending، در قالب یک Hook ارائه می‌دهد؛ این مقدار برای نمایش یک نشانگر بصری ملایم، نه یک حالت Loading کامل مثل فصل چهاردهم، مناسب است. ترکیب آن با Suspense، به حفظ نرم‌تر محتوای قبلی در طول یک به‌روزرسانی سنگین کمک می‌کند. در درس بعدی، به useDeferredValue، ابزار دیگری با هدفی مشابه اما نحوی متفاوت، می‌پردازیم.