Suspense و Data Loading: نگاهی دقیق‌تر به دریافت داده مبتنی‌بر Suspense

در درس قبل، به‌طور مختصر اشاره کردیم که دریافت داده هم می‌تواند از مکانیزم Suspense استفاده کند. در این درس، این موضوع را با جزئیات بیشتری باز می‌کنیم: چرا fetch خام این قابلیت را ندارد، منابع داده سازگار با Suspense دقیقاً چه کاری متفاوت انجام می‌دهند، و این رویکرد چه تفاوتی با الگوی دستی فصل چهاردهم دارد.

یادآوری مشکل اصلی fetch خام

fetch، یک Promise برمی‌گرداند؛ اما همان‌طور که در درس قبل با مثال createResource دیدیم، یک Promise به‌تنهایی نمی‌تواند یک کامپوننت را Suspend کند. React فقط می‌تواند با چیزی که مستقیماً، در حین Render، پرتاب (throw) شود، کار کند؛ نه با یک Promise که در یک useEffect و با .then() مدیریت می‌شود.

تفاوت بنیادین: خواندن در حین Render، نه پس از آن

در الگوی فصل چهاردهم، دریافت داده همیشه داخل یک useEffect، یعنی بعد از Render، آغاز می‌شد. در مدل Suspense، ایده برعکس است: کامپوننت، در همان حین محاسبه JSX خودش (در Render Phase)، تلاش می‌کند داده را «بخواند»؛ اگر داده آماده نباشد، همان لحظه، یک Promise پرتاب می‌شود:

jsx
function ProfileName() {
  const user = userResource.read(); // اگر آماده نباشد، throw می‌کند
  return <p>{user.name}</p>;
}

این دقیقاً همان الگویی بود که در مثال قابل اجرای درس قبل دیدیم؛ متد read()، بسته به وضعیت داخلی Promise، یا نتیجه را برمی‌گرداند، یا خود Promise را پرتاب می‌کند تا نزدیک‌ترین Suspense آن را بگیرد.

چرا نوشتن این منطق به‌صورت دستی توصیه نمی‌شود

مثال createResource در درس قبل، فقط برای نشان دادن مکانیزم پشت‌صحنه بود؛ در عمل، نوشتن این الگو به‌صورت دستی، برای هر درخواست، چند مشکل دارد که قبلاً هم دیده‌ایم: Race Condition (فصل ششم و چهاردهم)، Caching (فصل چهاردهم)، و مدیریت Cleanup. به همین دلیل، در عمل تقریباً همیشه از کتابخانه‌های تخصصی Server State استفاده می‌شود که این «Resource» را به‌درستی و با پوشش کامل این مسائل، پشت صحنه می‌سازند.

چگونگی هماهنگی Error با Suspense

در همان الگوی read()، اگر وضعیت داخلی Promise برابر خطا باشد، به‌جای داده، خود شیء خطا پرتاب می‌شود؛ این خطا، دیگر داخل جریان Suspense نیست، بلکه یک Runtime Error معمولی (فصل هفدهم) است و باید توسط یک Error Boundary گرفته شود؛ دقیقاً همان الگوی ترکیبی Error Boundary و Suspense که در درس چهارم این فصل دیدیم.

مقایسه مستقیم با الگوی دستی فصل چهاردهم

معیار الگوی دستی (فصل چهاردهم) الگوی مبتنی‌بر Suspense
محل شروع درخواست useEffect، پس از Render در حین خود Render
نمایش Loading State محلی در خود کامپوننت نزدیک‌ترین Suspense بیرونی
نمایش خطا State محلی در خود کامپوننت نزدیک‌ترین Error Boundary بیرونی

یک مزیت مهم: هماهنگی طبیعی چند منبع داده مستقل

وقتی چند کامپوننت مختلف، هرکدام داده جداگانه‌ای می‌خوانند و همگی داخل یک Suspense مشترک قرار دارند، طبق چیزی که در درس دوم این فصل دیدیم، همه آن‌ها با هم و هماهنگ (نه به‌شکل پراکنده و یکی‌یکی) ظاهر می‌شوند؛ چیزی که پیاده‌سازی دستی آن، با چند State جداگانه Loading، نیازمند هماهنگ‌سازی دستی اضافه (مثل ترکیب چند boolean با هم) بود.

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

در این مثال، دو منبع داده مستقل، داخل یک Suspense مشترک، با هم ظاهر می‌شوند؛ حتی با اینکه یکی سریع‌تر از دیگری آماده است.

هماهنگی دو منبع داده مستقل داخل یک Suspense مشترک
import { Suspense } from "react";

function createResource(promise) {
  let status = "pending";
  let result;
  const suspender = promise.then(
    (data) => { status = "success"; result = data; },
    (error) => { status = "error"; result = error; }
  );
  return {
    read() {
      if (status === "pending") throw suspender;
      if (status === "error") throw result;
      return result;
    },
  };
}

const nameResource = createResource(
  new Promise((resolve) => setTimeout(() => resolve("سارا"), 600))
);
const statsResource = createResource(
  new Promise((resolve) => setTimeout(() => resolve({ posts: 12 }), 1800))
);

function Name() {
  return <p>نام: {nameResource.read()}</p>;
}

function Stats() {
  return <p>تعداد پست‌ها: {statsResource.read().posts}</p>;
}

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

جمع‌بندی

در مدل Suspense، کامپوننت تلاش می‌کند داده را در حین Render بخواند و در صورت ناآماده بودن، یک Promise پرتاب می‌کند تا نزدیک‌ترین Suspense آن را بگیرد؛ خطا، جداگانه و توسط Error Boundary مدیریت می‌شود. این الگو، نوشتن دستی‌اش پرریسک است و در عمل به کتابخانه‌های تخصصی سپرده می‌شود، اما مزیت هماهنگ‌سازی طبیعی چند منبع داده مستقل را می‌دهد. در درس بعدی، به محدودیت‌ها و مدل ذهنی نهایی Suspense می‌پردازیم.