Suspense Boundary: رفتار دقیق مرز Suspense

در درس قبل، Suspense را به‌عنوان یک مرز برای وضعیت «هنوز آماده نیست» معرفی کردیم. در این درس، به جزئیات دقیق‌تر رفتار این مرز می‌پردازیم: چه چیزی داخل آن قرار می‌گیرد، نزدیک‌ترین مرز چگونه انتخاب می‌شود، و چه تفاوتی بین Suspend شدن در Mount اول و در یک Update وجود دارد.

یادآوری ساختار پایه

jsx
<Suspense fallback={<Spinner />}>
  <ProfileSection />
</Suspense>

اگر ProfileSection (یا هر فرزندی در عمق آن) هنوز آماده نباشد، کل محتوای داخل این Suspense، به‌طور کامل، با fallback جایگزین می‌شود؛ نه فقط همان یک کامپوننت ناآماده.

نزدیک‌ترین Suspense، همان قاعده Error Boundary

وقتی یک کامپوننت داخل درخت، وضعیت ناآماده بودن را اعلام می‌کند، React به سمت بالا حرکت می‌کند تا نزدیک‌ترین Suspense را پیدا کند؛ دقیقاً همان قاعده‌ای که در فصل هفدهم برای یافتن نزدیک‌ترین Error Boundary دیدیم. اگر چند Suspense تودرتو وجود داشته باشد (مثل مثالی که در فصل بیستم دیدیم)، فقط همان نزدیک‌ترین مرز، fallback خودش را نشان می‌دهد؛ مرزهای بیرونی‌تر، تحت‌تأثیر قرار نمی‌گیرند.

تفاوت مهم: Suspend شدن در اولین Mount در برابر یک Update

وقتی یک کامپوننت، برای اولین بار (در Mount) هنوز آماده نیست، رفتار واضح است: کل آن بخش، فوراً با fallback جایگزین می‌شود، چون چیزی از قبل روی صفحه نبوده که بخواهیم حفظش کنیم. اما وقتی کامپوننتی که از قبل روی صفحه بوده و با داده‌ای قدیمی به‌خوبی Render شده، به‌خاطر یک Update (مثلاً تغییر یک پارامتر) دوباره Suspend شود، نمایش فوری fallback به‌جای محتوای قبلی، می‌تواند تجربه‌ای ناخوشایند و پرش‌دار بسازد؛ این دقیقاً همان مشکلی است که در فصل چهاردهم، هنگام بحث درباره Loading اولیه در برابر Loading هنگام تغییر، به آن اشاره کردیم.

راه‌حل برای این مشکل: useTransition

برای کنترل دقیق‌تر این رفتار در یک Update، به ابزار دیگری نیاز است که به React اجازه می‌دهد محتوای قبلی را کمی بیشتر نگه دارد، به‌جای پریدن فوری به fallback. این ابزار، useTransition نام دارد و موضوع کامل فصل بیست‌ودوم، درباره Concurrent Rendering، خواهد بود؛ در این فصل فقط با رفتار پیش‌فرض Suspense سروکار داریم.

Suspense و لایه‌بندی با Error Boundary

Suspense و Error Boundary، دو مرز کاملاً مستقل‌اند که می‌توانند در کنار هم قرار بگیرند؛ یکی وضعیت «هنوز آماده نیست» را می‌گیرد، دیگری وضعیت «با خطا مواجه شد» را:

jsx
<ErrorBoundary fallback={<p>مشکلی پیش آمد.</p>}>
  <Suspense fallback={<p>در حال بارگذاری...</p>}>
    <ProfileSection />
  </Suspense>
</ErrorBoundary>

اگر ProfileSection هنوز آماده نباشد، Suspense فعال می‌شود؛ اگر در حین آماده شدن یا بعدش خطایی رخ دهد، Error Boundary بیرونی آن را می‌گیرد. این ترتیب قرارگیری (Error Boundary بیرون‌تر از Suspense) رایج‌ترین الگوست، چون معمولاً می‌خواهیم حتی اگر محتوای داخل Suspense با خطا مواجه شود، همچنان یک پیام معنادار ببینیم، نه این‌که خطا به‌کل از مرز کوچک‌تر عبور کند.

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

دو مرز Suspense مستقل با نزدیک‌ترین تطبیق
import { Suspense, lazy } from "react";

function delayed(text, ms) {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ default: () => <p>{text}</p> }), ms);
  });
}

const Sidebar = lazy(() => delayed("نوار کناری آماده شد", 500));
const MainArea = lazy(() => delayed("ناحیه اصلی آماده شد", 1500));

function App() {
  return (
    <div>
      <Suspense fallback={<p>در حال بارگذاری نوار کناری...</p>}>
        <Sidebar />
      </Suspense>
      <Suspense fallback={<p>در حال بارگذاری ناحیه اصلی...</p>}>
        <MainArea />
      </Suspense>
    </div>
  );
}
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

نزدیک‌ترین Suspense، دقیقاً مثل نزدیک‌ترین Error Boundary، مسئول کل محتوای داخل خودش می‌شود؛ رفتار پیش‌فرض برای Suspend شدن در یک Update، نمایش فوری fallback به‌جای محتوای قبلی است، که کنترل دقیق‌تر آن به useTransition نیاز دارد. Suspense و Error Boundary می‌توانند مستقل و در کنار هم استفاده شوند. در درس بعدی، به طراحی دقیق‌تر خود fallback می‌پردازیم.