فارسی

React Suspense را برای واکشی داده فراتر از تقسیم کد کاوش کنید. با Fetch-As-You-Render، مدیریت خطا و الگوهای آینده‌نگر برای اپلیکیشن‌های جهانی آشنا شوید.

بارگذاری منابع با React Suspense: تسلط بر الگوهای مدرن واکشی داده

در دنیای پویای توسعه وب، تجربه کاربری (UX) حرف اول را می‌زند. انتظار می‌رود اپلیکیشن‌ها سریع، واکنش‌گرا و لذت‌بخش باشند، صرف‌نظر از شرایط شبکه یا قابلیت‌های دستگاه. برای توسعه‌دهندگان ری‌اکت، این موضوع اغلب به مدیریت وضعیت پیچیده، نمایشگرهای بارگذاری پیچیده و مبارزه‌ای دائمی با آبشارهای واکشی داده (data fetching waterfalls) منجر می‌شود. اینجا است که React Suspense وارد می‌شود؛ یک ویژگی قدرتمند، هرچند اغلب به اشتباه درک شده، که برای تغییر بنیادین نحوه مدیریت عملیات ناهمزمان، به‌ویژه واکشی داده، طراحی شده است.

Suspense که در ابتدا برای تقسیم کد (code splitting) با React.lazy() معرفی شد، پتانسیل واقعی خود را در توانایی هماهنگ‌سازی بارگذاری *هر* منبع ناهمزمانی، از جمله داده‌های یک API، نشان می‌دهد. این راهنمای جامع به عمق React Suspense برای بارگذاری منابع می‌پردازد و مفاهیم اصلی، الگوهای بنیادی واکشی داده و ملاحظات عملی برای ساخت اپلیکیشن‌های جهانی با کارایی و انعطاف‌پذیری بالا را بررسی می‌کند.

تکامل واکشی داده در ری‌اکت: از دستوری به اعلانی

برای سال‌های متمادی، واکشی داده در کامپوننت‌های ری‌اکت عمدتاً بر یک الگوی رایج متکی بود: استفاده از هوک useEffect برای شروع یک فراخوانی API، مدیریت وضعیت‌های بارگذاری و خطا با useState، و رندر شرطی بر اساس این وضعیت‌ها. این رویکرد اگرچه کاربردی است، اما اغلب منجر به چندین چالش می‌شود:

یک سناریوی معمولی واکشی داده بدون Suspense را در نظر بگیرید:

import React, { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUser = async () => {
      try {
        setIsLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const data = await response.json();
        setUser(data);
      } catch (e) {
        setError(e);
      } finally {
        setIsLoading(false);
      }
    };
    fetchUser();
  }, [userId]);

  if (isLoading) {
    return <p>در حال بارگذاری پروفایل کاربر...</p>;
  }

  if (error) {
    return <p style={"color: red;"}>خطا: {error.message}</p>;
  }

  if (!user) {
    return <p>داده‌ای برای کاربر موجود نیست.</p>;
  }

  return (
    <div>
      <h2>کاربر: {user.name}</h2>
      <p>ایمیل: {user.email}</p>
      <!-- جزئیات بیشتر کاربر -->
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>به اپلیکیشن خوش آمدید</h1>
      <UserProfile userId={"123"} />
    </div>
  );
}

این الگو بسیار رایج است، اما کامپوننت را مجبور می‌کند تا وضعیت ناهمزمان خود را مدیریت کند، که اغلب منجر به یک رابطه شدیداً جفت‌شده (tightly coupled) بین UI و منطق واکشی داده می‌شود. Suspense یک جایگزین اعلانی‌تر و ساده‌تر ارائه می‌دهد.

درک React Suspense فراتر از تقسیم کد

بیشتر توسعه‌دهندگان اولین بار با Suspense از طریق React.lazy() برای تقسیم کد مواجه می‌شوند، که به شما اجازه می‌دهد بارگذاری کد یک کامپوننت را تا زمانی که به آن نیاز است به تعویق بیندازید. برای مثال:

import React, { Suspense, lazy } from 'react';

const LazyComponent = lazy(() => import('./MyHeavyComponent'));

function App() {
  return (
    <Suspense fallback={<div>در حال بارگذاری کامپوننت...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

در این سناریو، اگر MyHeavyComponent هنوز بارگذاری نشده باشد، مرز <Suspense> پرامیس (promise) پرتاب شده توسط lazy() را دریافت کرده و fallback را تا زمانی که کد کامپوننت آماده شود، نمایش می‌دهد. نکته کلیدی در اینجا این است که Suspense با گرفتن پرامیس‌هایی که در حین رندر پرتاب می‌شوند، کار می‌کند.

این مکانیزم مختص بارگذاری کد نیست. هر تابعی که در حین رندر فراخوانی شود و یک پرامیس پرتاب کند (مثلاً به دلیل اینکه یک منبع هنوز در دسترس نیست) می‌تواند توسط یک مرز Suspense در سطحی بالاتر از درخت کامپوننت گرفته شود. وقتی پرامیس حل (resolve) می‌شود، ری‌اکت سعی می‌کند کامپوننت را دوباره رندر کند، و اگر منبع اکنون در دسترس باشد، fallback پنهان شده و محتوای واقعی نمایش داده می‌شود.

مفاهیم اصلی Suspense برای واکشی داده

برای بهره‌برداری از Suspense برای واکشی داده، باید چند اصل اساسی را درک کنیم:

۱. پرتاب یک پرامیس

برخلاف کد ناهمزمان سنتی که از async/await برای حل پرامیس‌ها استفاده می‌کند، Suspense به تابعی متکی است که اگر داده‌ها آماده نباشند، یک پرامیس را *پرتاب* می‌کند. وقتی ری‌اکت سعی می‌کند یک کامپوننت را رندر کند که چنین تابعی را فراخوانی می‌کند و داده‌ها هنوز در حال انتظار (pending) هستند، پرامیس پرتاب می‌شود. سپس ری‌اکت رندر آن کامپوننت و فرزندانش را «متوقف» می‌کند و به دنبال نزدیک‌ترین مرز <Suspense> می‌گردد.

۲. مرز Suspense

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

یک مرز Suspense واحد می‌تواند چندین عملیات ناهمزمان را مدیریت کند. به عنوان مثال، اگر دو کامپوننت در یک مرز <Suspense> داشته باشید و هر کدام نیاز به واکشی داده داشته باشند، fallback تا زمانی که *هر دو* واکشی داده کامل شوند، نمایش داده خواهد شد. این کار از نمایش UI ناقص جلوگیری کرده و تجربه‌ی بارگذاری هماهنگ‌تری را فراهم می‌کند.

۳. مدیر کش/منبع (مسئولیت Userland)

نکته حیاتی این است که Suspense خود به تنهایی واکشی یا کش کردن داده را انجام نمی‌دهد. این فقط یک مکانیزم هماهنگ‌سازی است. برای اینکه Suspense برای واکشی داده کار کند، به یک لایه نیاز دارید که:

این «مدیر منبع» معمولاً با استفاده از یک کش ساده (مثلاً یک Map یا یک شیء) برای ذخیره وضعیت هر منبع (در حال انتظار، حل شده یا خطا) پیاده‌سازی می‌شود. در حالی که می‌توانید این را به صورت دستی برای اهداف نمایشی بسازید، در یک اپلیکیشن واقعی، از یک کتابخانه واکشی داده قوی که با Suspense یکپارچه شده است، استفاده خواهید کرد.

۴. حالت همزمان (Concurrent Mode) (پیشرفت‌های React 18)

در حالی که Suspense را می‌توان در نسخه‌های قدیمی‌تر ری‌اکت استفاده کرد، قدرت کامل آن با Concurrent React (که به طور پیش‌فرض در React 18 با createRoot فعال است) آزاد می‌شود. حالت همزمان به ری‌اکت اجازه می‌دهد تا کارهای رندر را قطع، متوقف و از سر بگیرد. این به این معناست:

الگوهای واکشی داده با Suspense

بیایید تکامل الگوهای واکشی داده با ظهور Suspense را بررسی کنیم.

الگوی ۱: واکشی-سپس-رندر (Fetch-Then-Render) (روش سنتی با پوشش Suspense)

این رویکرد کلاسیک است که در آن داده‌ها واکشی شده و تنها پس از آن کامپوننت رندر می‌شود. در حالی که از مکانیزم «پرتاب پرامیس» مستقیماً برای داده‌ها استفاده نمی‌کند، می‌توانید یک کامپوننت را که *در نهایت* داده‌ها را رندر می‌کند در یک مرز Suspense بپیچید تا یک fallback ارائه دهید. این بیشتر به استفاده از Suspense به عنوان یک هماهنگ‌کننده UI بارگذاری عمومی برای کامپوننت‌هایی است که در نهایت آماده می‌شوند، حتی اگر واکشی داده داخلی آنها هنوز بر اساس useEffect سنتی باشد.

import React, { Suspense, useState, useEffect } from 'react';

function UserDetails({ userId }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const fetchUserData = async () => {
      setIsLoading(true);
      const res = await fetch(`/api/users/${userId}`);
      const data = await res.json();
      setUser(data);
      setIsLoading(false);
    };
    fetchUserData();
  }, [userId]);

  if (isLoading) {
    return <p>در حال بارگذاری جزئیات کاربر...</p>;
  }

  return (
    <div>
      <h3>کاربر: {user.name}</h3>
      <p>ایمیل: {user.email}</p>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>مثال واکشی-سپس-رندر</h1>
      <Suspense fallback={<div>در حال بارگذاری کلی صفحه...</div>}>
        <UserDetails userId={"1"} />
      </Suspense>
    </div>
  );
}

مزایا: درک آسان، سازگار با نسخه‌های قبلی. می‌توان از آن به عنوان یک راه سریع برای افزودن یک وضعیت بارگذاری سراسری استفاده کرد.

معایب: کدهای تکراری داخل UserDetails را حذف نمی‌کند. هنوز هم در معرض آبشارها قرار دارد اگر کامپوننت‌ها داده‌ها را به صورت متوالی واکشی کنند. واقعاً از مکانیزم «پرتاب-و-گرفتن» Suspense برای خود داده‌ها استفاده نمی‌کند.

الگوی ۲: رندر-سپس-واکشی (Render-Then-Fetch) (واکشی داخل رندر، نه برای تولید)

این الگو عمدتاً برای نشان دادن کاری است که نباید مستقیماً با Suspense انجام داد، زیرا اگر به دقت مدیریت نشود، می‌تواند منجر به حلقه‌های بی‌نهایت یا مشکلات عملکردی شود. این شامل تلاش برای واکشی داده یا فراخوانی یک تابع معلق‌کننده مستقیماً در فاز رندر یک کامپوننت، *بدون* یک مکانیزم کش مناسب است.

// این را در محیط تولید بدون یک لایه کش مناسب استفاده نکنید
// این صرفاً برای نشان دادن این است که یک 'پرتاب' مستقیم چگونه می‌تواند به صورت مفهومی کار کند.

let fetchedData = null;
let dataPromise = null;

function fetchDataSynchronously(url) {
  if (fetchedData) {
    return fetchedData;
  }

  if (!dataPromise) {
    dataPromise = fetch(url)
      .then(res => res.json())
      .then(data => { fetchedData = data; dataPromise = null; return data; })
      .catch(err => { dataPromise = null; throw err; });
  }
  throw dataPromise; // اینجا جایی است که Suspense وارد عمل می‌شود
}

function UserDetailsBadExample({ userId }) {
  const user = fetchDataSynchronously(`/api/users/${userId}`);
  return (
    <div>
      <h3>کاربر: {user.name}</h3>
      <p>ایمیل: {user.email}</p>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>رندر-سپس-واکشی (توضیحی، مستقیماً توصیه نمی‌شود)</h1>
      <Suspense fallback={<div>در حال بارگذاری کاربر...</div>}>
        <UserDetailsBadExample userId={"2"} />
      </Suspense>
    </div>
  );
}

مزایا: نشان می‌دهد که چگونه یک کامپوننت می‌تواند مستقیماً داده‌ها را «درخواست» کند و در صورت عدم آمادگی معلق شود.

معایب: برای تولید بسیار مشکل‌ساز است. این سیستم دستی و سراسری fetchedData و dataPromise ساده است و درخواست‌های متعدد، بی‌اعتبار کردن یا وضعیت‌های خطا را به طور قوی مدیریت نمی‌کند. این یک نمایش ابتدایی از مفهوم «پرتاب-یک-پرامیس» است، نه یک الگو برای اتخاذ.

الگوی ۳: واکشی-همزمان-با-رندر (Fetch-As-You-Render) (الگوی ایده‌آل Suspense)

این یک تغییر پارادایم است که Suspense واقعاً برای واکشی داده امکان‌پذیر می‌کند. به جای انتظار برای رندر شدن یک کامپوننت قبل از واکشی داده‌هایش، یا واکشی تمام داده‌ها از قبل، واکشی-همزمان-با-رندر به این معنی است که شما واکشی داده را *در اسرع وقت* شروع می‌کنید، اغلب *قبل از* یا *همزمان با* فرآیند رندر. سپس کامپوننت‌ها داده‌ها را از یک کش «می‌خوانند»، و اگر داده‌ها آماده نباشند، معلق می‌شوند. ایده اصلی جدا کردن منطق واکشی داده از منطق رندر کامپوننت است.

برای پیاده‌سازی واکشی-همزمان-با-رندر، به مکانیزمی نیاز دارید تا:

  1. یک واکشی داده را خارج از تابع رندر کامپوننت آغاز کنید (مثلاً، هنگام ورود به یک مسیر، یا کلیک روی یک دکمه).
  2. پرامیس یا داده‌های حل شده را در یک کش ذخیره کنید.
  3. روشی برای کامپوننت‌ها فراهم کنید تا از این کش «بخوانند». اگر داده‌ها هنوز در دسترس نباشند، تابع خواندن، پرامیس در حال انتظار را پرتاب می‌کند.

این الگو مشکل آبشار را حل می‌کند. اگر دو کامپوننت مختلف به داده نیاز داشته باشند، درخواست‌های آنها می‌توانند به صورت موازی آغاز شوند و UI تنها زمانی ظاهر می‌شود که *هر دو* آماده باشند، که توسط یک مرز Suspense واحد هماهنگ می‌شود.

پیاده‌سازی دستی (برای درک)

برای درک مکانیک زیربنایی، بیایید یک مدیر منبع دستی ساده‌سازی شده ایجاد کنیم. در یک اپلیکیشن واقعی، از یک کتابخانه اختصاصی استفاده خواهید کرد.

import React, { Suspense } from 'react';

// --- مدیر کش/منبع ساده --- //
const cache = new Map();

function createResource(promise) {
  let status = 'pending';
  let result;
  let suspender = promise.then(
    (r) => {
      status = 'success';
      result = r;
    },
    (e) => {
      status = 'error';
      result = e;
    }
  );

  return {
    read() {
      if (status === 'pending') {
        throw suspender;
      } else if (status === 'error') {
        throw result;
      } else if (status === 'success') {
        return result;
      }
    },
  };
}

function fetchData(key, fetcher) {
  if (!cache.has(key)) {
    cache.set(key, createResource(fetcher()));
  }
  return cache.get(key);
}

// --- توابع واکشی داده --- //
const fetchUserById = (id) => {
  console.log(`در حال واکشی کاربر ${id}...`);
  return new Promise(resolve => setTimeout(() => {
    const users = {
      '1': { id: '1', name: 'آلیس اسمیت', email: 'alice@example.com' },
      '2': { id: '2', name: 'باب جانسون', email: 'bob@example.com' },
      '3': { id: '3', name: 'چارلی براون', email: 'charlie@example.com' }
    };
    resolve(users[id]);
  }, 1500));
};

const fetchPostsByUserId = (userId) => {
  console.log(`در حال واکشی پست‌ها برای کاربر ${userId}...`);
  return new Promise(resolve => setTimeout(() => {
    const posts = {
      '1': [{ id: 'p1', title: 'اولین پست من' }, { id: 'p2', title: 'ماجراهای سفر' }],
      '2': [{ id: 'p3', title: 'بینش‌های برنامه‌نویسی' }],
      '3': [{ id: 'p4', title: 'روندهای جهانی' }, { id: 'p5', title: 'غذاهای محلی' }]
    };
    resolve(posts[userId] || []);
  }, 2000));
};

// --- کامپوننت‌ها --- //
function UserProfile({ userId }) {
  const userResource = fetchData(`user-${userId}`, () => fetchUserById(userId));
  const user = userResource.read(); // اگر داده‌های کاربر آماده نباشد، این معلق خواهد شد

  return (
    <div>
      <h3>کاربر: {user.name}</h3>
      <p>ایمیل: {user.email}</p>
    </div>
  );
}

function UserPosts({ userId }) {
  const postsResource = fetchData(`posts-${userId}`, () => fetchPostsByUserId(userId));
  const posts = postsResource.read(); // اگر داده‌های پست‌ها آماده نباشد، این معلق خواهد شد

  return (
    <div>
      <h4>پست‌های {userId}:</h4>
      <ul>
        {posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
        {posts.length === 0 && <li>هیچ پستی یافت نشد.</li>}
      </ul>
    </div>
  );
}

// --- اپلیکیشن --- //
let initialUserResource = null;
let initialPostsResource = null;

function prefetchDataForUser(userId) {
  initialUserResource = fetchData(`user-${userId}`, () => fetchUserById(userId));
  initialPostsResource = fetchData(`posts-${userId}`, () => fetchPostsByUserId(userId));
}

// پیش-واکشی برخی داده‌ها حتی قبل از رندر شدن کامپوننت App
prefetchDataForUser('1');

function App() {
  return (
    <div>
      <h1>واکشی-همزمان-با-رندر با Suspense</h1>
      <p>این نشان می‌دهد که چگونه واکشی داده می‌تواند به صورت موازی و هماهنگ توسط Suspense انجام شود.</p>

      <Suspense fallback={<div>در حال بارگذاری پروفایل کاربر و پست‌ها...</div>}>
        <UserProfile userId={"1"} />
        <UserPosts userId={"1"} />
      </Suspense>

      <h2>بخش دیگر</h2>
      <Suspense fallback={<div>در حال بارگذاری کاربری دیگر...</div>}>
        <UserProfile userId={"2"} />
      </Suspense>
    </div>
  );
}

در این مثال:

کتابخانه‌ها برای واکشی-همزمان-با-رندر

ساخت و نگهداری یک مدیر منبع قوی به صورت دستی پیچیده است. خوشبختانه، چندین کتابخانه واکشی داده بالغ، Suspense را پذیرفته‌اند یا در حال پذیرش آن هستند و راه‌حل‌های آزمایش شده و قوی ارائه می‌دهند:

این کتابخانه‌ها پیچیدگی‌های ایجاد و مدیریت منابع، مدیریت کش، اعتبارسنجی مجدد، به‌روزرسانی‌های خوش‌بینانه و مدیریت خطا را انتزاعی می‌کنند و پیاده‌سازی واکشی-همزمان-با-رندر را بسیار آسان‌تر می‌سازند.

الگوی ۴: پیش-واکشی (Prefetching) با کتابخانه‌های آگاه از Suspense

پیش-واکشی یک بهینه‌سازی قدرتمند است که در آن شما به صورت فعال داده‌هایی را که احتمالاً یک کاربر در آینده نزدیک به آنها نیاز خواهد داشت، قبل از اینکه حتی به صراحت درخواست کند، واکشی می‌کنید. این کار می‌تواند عملکرد درک شده را به شدت بهبود بخشد.

با کتابخانه‌های آگاه از Suspense، پیش-واکشی یکپارچه می‌شود. شما می‌توانید واکشی داده را بر اساس تعاملات کاربر که بلافاصله UI را تغییر نمی‌دهند، مانند نگه داشتن ماوس روی یک لینک یا دکمه، فعال کنید.

import React, { Suspense } from 'react';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';

// فرض کنید اینها فراخوانی‌های API شما هستند
const fetchProductById = async (id) => {
  console.log(`در حال واکشی محصول ${id}...`);
  return new Promise(resolve => setTimeout(() => {
    const products = {
      'A001': { id: 'A001', name: 'ابزار جهانی X', price: 29.99, description: 'یک ابزار چندکاره برای استفاده بین‌المللی.' },
      'B002': { id: 'B002', name: 'گجت همگانی Y', price: 149.99, description: 'گجت پیشرفته، محبوب در سراسر جهان.' },
    };
    resolve(products[id]);
  }, 1000));
};

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      suspense: true, // فعال‌سازی Suspense برای تمام کوئری‌ها به صورت پیش‌فرض
    },
  },
});

function ProductDetails({ productId }) {
  const { data: product } = useQuery({
    queryKey: ['product', productId],
    queryFn: () => fetchProductById(productId),
  });

  return (
    <div style={{"border": "1px solid #ccc", "padding": "15px", "margin": "10px 0"}}>
      <h3>{product.name}</h3>
      <p>قیمت: ${product.price.toFixed(2)}</p>
      <p>{product.description}</p>
    </div>
  );
}

function ProductList() {
  const handleProductHover = (productId) => {
    // پیش-واکشی داده زمانی که کاربر ماوس را روی لینک محصول نگه می‌دارد
    queryClient.prefetchQuery({
      queryKey: ['product', productId],
      queryFn: () => fetchProductById(productId),
    });
    console.log(`پیش-واکشی محصول ${productId}`);
  };

  return (
    <div>
      <h2>محصولات موجود:</h2>
      <ul>
        <li>
          <a href="#" onMouseEnter={() => handleProductHover('A001')}
             onClick={(e) => { e.preventDefault(); /* ناوبری یا نمایش جزئیات */ }}
          >ابزار جهانی X (A001)</a>
        </li>
        <li>
          <a href="#" onMouseEnter={() => handleProductHover('B002')}
             onClick={(e) => { e.preventDefault(); /* ناوبری یا نمایش جزئیات */ }}
          >گجت همگانی Y (B002)</a>
        </li>
      </ul>
      <p>ماوس را روی لینک محصول نگه دارید تا پیش-واکشی را در عمل ببینید. تب شبکه را برای مشاهده باز کنید.</p>
    </div>
  );
}

function App() {
  const [showProductA, setShowProductA] = React.useState(false);
  const [showProductB, setShowProductB] = React.useState(false);

  return (
    <QueryClientProvider client={queryClient}>
      <h1>پیش-واکشی با React Suspense (React Query)</h1>
      <ProductList />

      <button onClick={() => setShowProductA(true)}>نمایش ابزار جهانی X</button>
      <button onClick={() => setShowProductB(true)}>نمایش گجت همگانی Y</button>

      {showProductA && (
        <Suspense fallback={<p>در حال بارگذاری ابزار جهانی X...</p>}>
          <ProductDetails productId="A001" />
        </Suspense>
      )}
      {showProductB && (
        <Suspense fallback={<p>در حال بارگذاری گجت همگانی Y...</p>}>
          <ProductDetails productId="B002" />
        </Suspense>
      )}
    </QueryClientProvider>
  );
}

در این مثال، نگه داشتن ماوس روی لینک محصول، `queryClient.prefetchQuery` را فعال می‌کند که واکشی داده را در پس‌زمینه آغاز می‌کند. اگر کاربر سپس روی دکمه‌ای کلیک کند تا جزئیات محصول را ببیند، و داده‌ها از پیش-واکشی در کش موجود باشند، کامپوننت بلافاصله بدون معلق شدن رندر می‌شود. اگر پیش-واکشی هنوز در حال انجام باشد یا آغاز نشده باشد، Suspense تا زمانی که داده‌ها آماده شوند، fallback را نمایش می‌دهد.

مدیریت خطا با Suspense و مرزهای خطا (Error Boundaries)

در حالی که Suspense وضعیت «بارگذاری» را با نمایش یک fallback مدیریت می‌کند، مستقیماً وضعیت «خطا» را مدیریت نمی‌کند. اگر یک پرامیس که توسط یک کامپوننت معلق پرتاب شده، رد (reject) شود (یعنی واکشی داده با شکست مواجه شود)، این خطا در درخت کامپوننت به بالا منتشر می‌شود. برای مدیریت این خطاها به صورت زیبا و نمایش یک UI مناسب، باید از مرزهای خطا استفاده کنید.

یک مرز خطا یک کامپوننت ری‌اکت است که یکی از متدهای چرخه حیات componentDidCatch یا static getDerivedStateFromError را پیاده‌سازی می‌کند. این کامپوننت خطاهای جاوااسکریپت را در هر جای درخت کامپوننت فرزند خود می‌گیرد، از جمله خطاهای پرتاب شده توسط پرامیس‌هایی که Suspense در حالت انتظار معمولاً آنها را می‌گرفت.

import React, { Suspense, useState } from 'react';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';

// --- کامپوننت مرز خطا --- //
class MyErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    // به‌روزرسانی وضعیت تا رندر بعدی UI جایگزین را نشان دهد.
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // همچنین می‌توانید خطا را به یک سرویس گزارش خطا ارسال کنید
    console.error("یک خطا گرفته شد:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // می‌توانید هر UI جایگزین سفارشی را رندر کنید
      return (
        <div style={{"border": "2px solid red", "padding": "20px", "margin": "20px 0", "background": "#ffe0e0"}}>
          <h2>مشکلی پیش آمده است!</h2>
          <p>{this.state.error && this.state.error.message}</p>
          <p>لطفاً صفحه را بازخوانی کنید یا با پشتیبانی تماس بگیرید.</p>
          <button onClick={() => this.setState({ hasError: false, error: null })}>دوباره امتحان کنید</button>
        </div>
      );
    }
    return this.props.children;
  }
}

// --- واکشی داده (با احتمال خطا) --- //
const fetchItemById = async (id) => {
  console.log(`تلاش برای واکشی آیتم ${id}...`);
  return new Promise((resolve, reject) => setTimeout(() => {
    if (id === 'error-item') {
      reject(new Error('بارگذاری آیتم ناموفق بود: شبکه در دسترس نیست یا آیتم یافت نشد.'));
    } else if (id === 'slow-item') {
      resolve({ id: 'slow-item', name: 'به کندی تحویل داده شد', data: 'این آیتم کمی طول کشید اما رسید!', status: 'success' });
    } else {
      resolve({ id, name: `آیتم ${id}`, data: `داده برای آیتم ${id}` });
    }
  }, id === 'slow-item' ? 3000 : 800));
};

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      suspense: true,
      retry: false, // برای نمایش، تلاش مجدد را غیرفعال کنید تا خطا فوری باشد
    },
  },
});

function DisplayItem({ itemId }) {
  const { data: item } = useQuery({
    queryKey: ['item', itemId],
    queryFn: () => fetchItemById(itemId),
  });

  return (
    <div>
      <h3>جزئیات آیتم:</h3>
      <p>شناسه: {item.id}</p>
      <p>نام: {item.name}</p>
      <p>داده: {item.data}</p>
    </div>
  );
}

function App() {
  const [fetchType, setFetchType] = useState('normal-item');

  return (
    <QueryClientProvider client={queryClient}>
      <h1>Suspense و مرزهای خطا</h1>

      <div>
        <button onClick={() => setFetchType('normal-item')}>واکشی آیتم عادی</button>
        <button onClick={() => setFetchType('slow-item')}>واکشی آیتم کند</button>
        <button onClick={() => setFetchType('error-item')}>واکشی آیتم با خطا</button>
      </div>

      <MyErrorBoundary>
        <Suspense fallback={<p>در حال بارگذاری آیتم از طریق Suspense...</p>}>
          <DisplayItem itemId={fetchType} />
        </Suspense>
      </MyErrorBoundary>
    </QueryClientProvider>
  );
}

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

مدیریت وضعیت و بی‌اعتبار کردن داده با Suspense

مهم است که روشن شود React Suspense عمدتاً وضعیت بارگذاری اولیه منابع ناهمزمان را مدیریت می‌کند. این به طور ذاتی کش سمت کلاینت را مدیریت نمی‌کند، بی‌اعتبار کردن داده را انجام نمی‌دهد، یا جهش‌ها (عملیات ایجاد، به‌روزرسانی، حذف) و به‌روزرسانی‌های UI متعاقب آنها را هماهنگ نمی‌کند.

اینجا جایی است که کتابخانه‌های واکشی داده آگاه از Suspense (React Query, SWR, Apollo Client, Relay) ضروری می‌شوند. آنها Suspense را با ارائه موارد زیر تکمیل می‌کنند:

بدون یک کتابخانه واکشی داده قوی، پیاده‌سازی این ویژگی‌ها بر روی یک مدیر منبع Suspense دستی، یک کار بزرگ خواهد بود و اساساً شما را ملزم به ساخت چارچوب واکشی داده خود می‌کند.

ملاحظات عملی و بهترین شیوه‌ها

پذیرش Suspense برای واکشی داده یک تصمیم معماری مهم است. در اینجا برخی از ملاحظات عملی برای یک اپلیکیشن جهانی آورده شده است:

۱. همه داده‌ها به Suspense نیاز ندارند

Suspense برای داده‌های حیاتی که مستقیماً بر رندر اولیه یک کامپوننت تأثیر می‌گذارند، ایده‌آل است. برای داده‌های غیر حیاتی، واکشی‌های پس‌زمینه، یا داده‌هایی که می‌توانند به صورت تنبل و بدون تأثیر بصری قوی بارگذاری شوند، useEffect سنتی یا پیش-رندر ممکن است هنوز مناسب باشند. استفاده بیش از حد از Suspense می‌تواند به یک تجربه بارگذاری کمتر دقیق منجر شود، زیرا یک مرز Suspense واحد منتظر حل شدن *تمام* فرزندانش می‌ماند.

۲. دانه‌بندی مرزهای Suspense

مرزهای <Suspense> خود را با دقت قرار دهید. یک مرز بزرگ و واحد در بالای اپلیکیشن شما ممکن است کل صفحه را پشت یک اسپینر پنهان کند که می‌تواند ناامیدکننده باشد. مرزهای کوچکتر و دقیق‌تر به بخش‌های مختلف صفحه شما اجازه می‌دهند تا به طور مستقل بارگذاری شوند و یک تجربه پیشرونده و واکنش‌گراتر ارائه دهند. به عنوان مثال، یک مرز در اطراف یک کامپوننت پروفایل کاربر و دیگری در اطراف لیستی از محصولات پیشنهادی.

<div>
  <h1>صفحه محصول</h1>
  <Suspense fallback={<p>در حال بارگذاری جزئیات اصلی محصول...</p>}>
    <ProductDetails id="prod123" />
  </Suspense>

  <hr />

  <h2>محصولات مرتبط</h2>
  <Suspense fallback={<p>در حال بارگذاری محصولات مرتبط...</p>}>
    <RelatedProducts category="electronics" />
  </Suspense>
</div>

این رویکرد به این معنی است که کاربران می‌توانند جزئیات اصلی محصول را حتی اگر محصولات مرتبط هنوز در حال بارگذاری باشند، ببینند.

۳. رندر سمت سرور (SSR) و HTML جریانی

APIهای جدید SSR جریانی React 18 (renderToPipeableStream) به طور کامل با Suspense یکپارچه شده‌اند. این به سرور شما اجازه می‌دهد تا HTML را به محض آماده شدن ارسال کند، حتی اگر بخش‌هایی از صفحه (مانند کامپوننت‌های وابسته به داده) هنوز در حال بارگذاری باشند. سرور می‌تواند یک جایگزین (از fallback Suspense) را به صورت جریانی ارسال کند و سپس محتوای واقعی را هنگامی که داده‌ها حل می‌شوند، بدون نیاز به رندر مجدد کامل سمت کلاینت، به صورت جریانی ارسال کند. این امر عملکرد بارگذاری درک شده را برای کاربران جهانی در شرایط مختلف شبکه به طور قابل توجهی بهبود می‌بخشد.

۴. پذیرش تدریجی

لازم نیست کل اپلیکیشن خود را برای استفاده از Suspense بازنویسی کنید. می‌توانید آن را به صورت تدریجی معرفی کنید، با شروع از ویژگی‌ها یا کامپوننت‌های جدیدی که بیشترین بهره را از الگوهای بارگذاری اعلانی آن می‌برند.

۵. ابزارها و اشکال‌زدایی

در حالی که Suspense منطق کامپوننت را ساده می‌کند، اشکال‌زدایی می‌تواند متفاوت باشد. React DevTools بینش‌هایی در مورد مرزهای Suspense و وضعیت‌های آنها ارائه می‌دهد. با نحوه نمایش وضعیت داخلی توسط کتابخانه واکشی داده انتخابی خود آشنا شوید (مثلاً، React Query Devtools).

۶. مهلت زمانی (Timeouts) برای fallbackهای Suspense

برای زمان‌های بارگذاری بسیار طولانی، ممکن است بخواهید یک مهلت زمانی به fallback Suspense خود اضافه کنید، یا پس از یک تأخیر مشخص به یک نشانگر بارگذاری دقیق‌تر تغییر دهید. هوک‌های useDeferredValue و useTransition در React 18 می‌توانند به مدیریت این وضعیت‌های بارگذاری دقیق‌تر کمک کنند، و به شما اجازه می‌دهند تا یک نسخه «قدیمی» از UI را در حالی که داده‌های جدید در حال واکشی هستند، نشان دهید یا به‌روزرسانی‌های غیرفوری را به تعویق بیندازید.

آینده واکشی داده در ری‌اکت: کامپوننت‌های سرور ری‌اکت و فراتر از آن

سفر واکشی داده در ری‌اکت با Suspense سمت کلاینت متوقف نمی‌شود. کامپوننت‌های سرور ری‌اکت (RSC) یک تکامل قابل توجه را نشان می‌دهند و قول می‌دهند که مرزهای بین کلاینت و سرور را محو کرده و واکشی داده را بیشتر بهینه کنند.

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

نتیجه‌گیری

React Suspense، که در ابتدا یک ویژگی برای تقسیم کد بود، به یک ابزار تحول‌آفرین برای واکشی داده تبدیل شده است. با پذیرش الگوی واکشی-همزمان-با-رندر و بهره‌گیری از کتابخانه‌های آگاه از Suspense، توسعه‌دهندگان می‌توانند تجربه کاربری اپلیکیشن‌های خود را به طور قابل توجهی بهبود بخشند، آبشارهای بارگذاری را حذف کنند، منطق کامپوننت را ساده کنند و وضعیت‌های بارگذاری روان و هماهنگ ارائه دهند. همراه با مرزهای خطا برای مدیریت خطای قوی و وعده آینده کامپوننت‌های سرور ری‌اکت، Suspense ما را قادر می‌سازد تا اپلیکیشن‌هایی بسازیم که نه تنها کارآمد و انعطاف‌پذیر هستند، بلکه ذاتاً برای کاربران در سراسر جهان لذت‌بخش‌ترند. تغییر به یک پارادایم واکشی داده مبتنی بر Suspense نیاز به یک تعدیل مفهومی دارد، اما مزایای آن از نظر وضوح کد، عملکرد و رضایت کاربر قابل توجه و ارزش سرمایه‌گذاری را دارد.