Suspense و Lazy Loading: نگاهی عمیق‌تر به ترکیب این دو

در فصل بیستم، Suspense را همراه با lazy دیدیم، اما تمرکز اصلی آن فصل روی خود Dynamic Import و Code Splitting بود. حالا که Suspense را به‌طور مستقل بهتر می‌شناسیم، در این درس به جزئیاتی از این ترکیب می‌پردازیم که در آن فصل فرصت پرداختن به آن‌ها نبود؛ از جمله رفتار در برابر خطای بارگذاری و الگوهای عملی بیشتر.

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

jsx
const Chart = lazy(() => import("./Chart"));

<Suspense fallback={<ChartSkeleton />}>
  <Chart />
</Suspense>

چه اتفاقی می‌افتد اگر خود دانلود شکست بخورد

Suspense فقط حالت «هنوز آماده نیست» را مدیریت می‌کند؛ اگر دانلود ماژول lazy، به دلیل قطعی شبکه یا خطای سرور، کاملاً شکست بخورد (نه این‌که فقط کمی طول بکشد)، این یک Runtime Error واقعی است، دقیقاً از همان نوعی که در فصل هفدهم دیدیم. به همین دلیل، همان‌طور که در درس قبل اشاره کردیم، ترکیب رایج و لازم، قرار دادن یک Error Boundary بیرون از Suspense است:

jsx
<ErrorBoundary fallback={<p>بارگذاری این بخش با مشکل مواجه شد. دوباره تلاش کنید.</p>}>
  <Suspense fallback={<ChartSkeleton />}>
    <Chart />
  </Suspense>
</ErrorBoundary>

بدون این Error Boundary، یک شکست شبکه در دانلود Chart، طبق همان رفتار پیش‌فرض فصل هفدهم، می‌تواند بخش بزرگ‌تری از برنامه را Unmount کند.

الگوی عملی: چند کامپوننت lazy مستقل، با مرزهای جدا

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

jsx
function Dashboard() {
  return (
    <div>
      <ErrorBoundary fallback={<p>نمودار در دسترس نیست.</p>}>
        <Suspense fallback={<ChartSkeleton />}>
          <Chart />
        </Suspense>
      </ErrorBoundary>

      <ErrorBoundary fallback={<p>جدول در دسترس نیست.</p>}>
        <Suspense fallback={<TableSkeleton />}>
          <DataTable />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

با این ساختار، اگر دانلود DataTable شکست بخورد، Chart کاملاً دست‌نخورده باقی می‌ماند؛ هر ویجت، هم از نظر بارگذاری و هم از نظر خطا، کاملاً مستقل از دیگری است.

ترکیب با Preloading که در فصل بیستم دیدیم

تکنیک شروع زودهنگام import() در onMouseEnter، که در فصل بیستم دیدیم، همچنان کاملاً با Suspense سازگار است؛ Preload فقط دانلود را زودتر شروع می‌کند، اما اگر کاربر زودتر از پایان دانلود کلیک کند، Suspense همچنان fallback را تا پایان واقعی دانلود نشان می‌دهد. این دو تکنیک، مکمل هم هستند، نه جایگزین یکدیگر.

یک یادآوری مهم: محدودیت امروز React.lazy

باید به یاد داشت که lazy و Suspense، فقط مشکل بارگذاری کد (خود فایل کامپوننت) را حل می‌کنند؛ اگر همان کامپوننت Chart، بعد از بارگذاری، خودش هم نیاز به دریافت داده از سرور داشته باشد، آن بخش از کار، همچنان یا باید با الگوی دستی فصل چهاردهم مدیریت شود، یا با منابع داده‌ای که به‌طور رسمی از Suspense پشتیبانی می‌کنند؛ چیزی که، همان‌طور که در درس اول این فصل اشاره شد، به زیرساخت خاصی نیاز دارد.

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

دو ویجت مستقل با Error Boundary و Suspense جداگانه
import { Component, Suspense, lazy } from "react";

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  static getDerivedStateFromError() {
    return { hasError: true };
  }
  render() {
    if (this.state.hasError) return this.props.fallback;
    return this.props.children;
  }
}

const Chart = lazy(() => new Promise((resolve) => {
  setTimeout(() => resolve({ default: () => <p>نمودار آماده شد.</p> }), 800);
}));

const BrokenTable = lazy(() => new Promise((_, reject) => {
  setTimeout(() => reject(new Error("خطای دانلود")), 800);
}));

function App() {
  return (
    <div>
      <ErrorBoundary fallback={<p>نمودار در دسترس نیست.</p>}>
        <Suspense fallback={<p>در حال بارگذاری نمودار...</p>}>
          <Chart />
        </Suspense>
      </ErrorBoundary>

      <ErrorBoundary fallback={<p>جدول در دسترس نیست.</p>}>
        <Suspense fallback={<p>در حال بارگذاری جدول...</p>}>
          <BrokenTable />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

Suspense، شکست کامل دانلود یک ماژول lazy را مدیریت نمی‌کند؛ این مورد، یک Runtime Error واقعی است که باید با یک Error Boundary بیرون از Suspense گرفته شود. در یک داشبورد با چند ویجت مستقل، معمولاً هر ویجت جفت مرز Suspense/Error Boundary خودش را دارد تا کاملاً مستقل از بقیه باشد. Preloading و Suspense، دو تکنیک مکمل‌اند، نه جایگزین هم. در درس بعدی، به کاربردهای مدرن‌تر Suspense می‌پردازیم.