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
، و رندر شرطی بر اساس این وضعیتها. این رویکرد اگرچه کاربردی است، اما اغلب منجر به چندین چالش میشود:
- تکثیر وضعیت بارگذاری: تقریباً هر کامپوننتی که به داده نیاز داشت، به وضعیتهای
isLoading
،isError
وdata
خود نیاز داشت که منجر به کدهای تکراری (boilerplate) میشد. - آبشارها و شرایط رقابتی (Race Conditions): کامپوننتهای تو در تو که داده واکشی میکردند، اغلب منجر به درخواستهای متوالی (آبشارها) میشدند؛ به این صورت که یک کامپوننت والد دادهها را واکشی میکرد، سپس رندر میشد، سپس یک کامپوننت فرزند دادههای خود را واکشی میکرد و این روند ادامه مییافت. این امر زمان کلی بارگذاری را افزایش میداد. همچنین ممکن بود شرایط رقابتی رخ دهد؛ زمانی که چندین درخواست آغاز میشد و پاسخها به ترتیب نامنظم میرسیدند.
- مدیریت خطای پیچیده: توزیع پیامهای خطا و منطق بازیابی در میان کامپوننتهای متعدد میتوانست دشوار باشد و به ارسال پراپها (prop drilling) یا راهحلهای مدیریت وضعیت سراسری نیاز داشت.
- تجربه کاربری ناخوشایند: ظاهر و ناپدید شدن چندین اسپینر یا تغییرات ناگهانی محتوا (layout shifts) میتوانست تجربهای ناخوشایند برای کاربران ایجاد کند.
- ارسال پراپ برای داده و وضعیت: ارسال دادههای واکشی شده و وضعیتهای بارگذاری/خطای مرتبط از طریق چندین لایه از کامپوننتها به یک منبع رایج پیچیدگی تبدیل شده بود.
یک سناریوی معمولی واکشی داده بدون 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 برای واکشی داده کار کند، به یک لایه نیاز دارید که:
- واکشی داده را آغاز کند.
- نتیجه را کش کند (دادههای حل شده یا پرامیس در حال انتظار).
- یک متد همزمان
read()
فراهم کند که یا دادههای کش شده را فوراً برمیگرداند (در صورت موجود بودن) یا پرامیس در حال انتظار را پرتاب میکند (در صورت عدم وجود).
این «مدیر منبع» معمولاً با استفاده از یک کش ساده (مثلاً یک Map یا یک شیء) برای ذخیره وضعیت هر منبع (در حال انتظار، حل شده یا خطا) پیادهسازی میشود. در حالی که میتوانید این را به صورت دستی برای اهداف نمایشی بسازید، در یک اپلیکیشن واقعی، از یک کتابخانه واکشی داده قوی که با Suspense یکپارچه شده است، استفاده خواهید کرد.
۴. حالت همزمان (Concurrent Mode) (پیشرفتهای React 18)
در حالی که Suspense را میتوان در نسخههای قدیمیتر ریاکت استفاده کرد، قدرت کامل آن با Concurrent React (که به طور پیشفرض در React 18 با createRoot
فعال است) آزاد میشود. حالت همزمان به ریاکت اجازه میدهد تا کارهای رندر را قطع، متوقف و از سر بگیرد. این به این معناست:
- بهروزرسانیهای UI بدون مسدود کردن: وقتی Suspense یک fallback نمایش میدهد، ریاکت میتواند به رندر کردن سایر بخشهای UI که معلق نیستند، ادامه دهد یا حتی UI جدید را در پسزمینه بدون مسدود کردن رشته اصلی آماده کند.
- انتقالها (Transitions): APIهای جدیدی مانند
useTransition
به شما اجازه میدهند تا بهروزرسانیهای خاصی را به عنوان «انتقال» علامتگذاری کنید که ریاکت میتواند آنها را قطع کرده و با اولویت کمتری انجام دهد، که باعث تغییرات UI روانتر در حین واکشی داده میشود.
الگوهای واکشی داده با 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 واقعاً برای واکشی داده امکانپذیر میکند. به جای انتظار برای رندر شدن یک کامپوننت قبل از واکشی دادههایش، یا واکشی تمام دادهها از قبل، واکشی-همزمان-با-رندر به این معنی است که شما واکشی داده را *در اسرع وقت* شروع میکنید، اغلب *قبل از* یا *همزمان با* فرآیند رندر. سپس کامپوننتها دادهها را از یک کش «میخوانند»، و اگر دادهها آماده نباشند، معلق میشوند. ایده اصلی جدا کردن منطق واکشی داده از منطق رندر کامپوننت است.
برای پیادهسازی واکشی-همزمان-با-رندر، به مکانیزمی نیاز دارید تا:
- یک واکشی داده را خارج از تابع رندر کامپوننت آغاز کنید (مثلاً، هنگام ورود به یک مسیر، یا کلیک روی یک دکمه).
- پرامیس یا دادههای حل شده را در یک کش ذخیره کنید.
- روشی برای کامپوننتها فراهم کنید تا از این کش «بخوانند». اگر دادهها هنوز در دسترس نباشند، تابع خواندن، پرامیس در حال انتظار را پرتاب میکند.
این الگو مشکل آبشار را حل میکند. اگر دو کامپوننت مختلف به داده نیاز داشته باشند، درخواستهای آنها میتوانند به صورت موازی آغاز شوند و 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>
);
}
در این مثال:
- توابع
createResource
وfetchData
یک مکانیزم کش اولیه را راهاندازی میکنند. - وقتی
UserProfile
یاUserPosts
تابعresource.read()
را فراخوانی میکنند، یا دادهها را فوراً دریافت میکنند یا پرامیس پرتاب میشود. - نزدیکترین مرز
<Suspense>
پرامیس(ها) را گرفته و fallback خود را نمایش میدهد. - نکته مهم این است که میتوانیم
prefetchDataForUser('1')
را *قبل از* رندر شدن کامپوننتApp
فراخوانی کنیم، که به واکشی داده اجازه میدهد حتی زودتر شروع شود.
کتابخانهها برای واکشی-همزمان-با-رندر
ساخت و نگهداری یک مدیر منبع قوی به صورت دستی پیچیده است. خوشبختانه، چندین کتابخانه واکشی داده بالغ، Suspense را پذیرفتهاند یا در حال پذیرش آن هستند و راهحلهای آزمایش شده و قوی ارائه میدهند:
- React Query (TanStack Query): یک لایه قدرتمند واکشی و کش داده با پشتیبانی از Suspense ارائه میدهد. هوکهایی مانند
useQuery
را فراهم میکند که میتوانند معلق شوند. برای APIهای REST عالی است. - SWR (Stale-While-Revalidate): یکی دیگر از کتابخانههای محبوب و سبک واکشی داده است که به طور کامل از Suspense پشتیبانی میکند. ایدهآل برای APIهای REST، بر روی ارائه سریع دادهها (کهنه) و سپس اعتبارسنجی مجدد آنها در پسزمینه تمرکز دارد.
- Apollo Client: یک کلاینت GraphQL جامع که یکپارچگی قوی با Suspense برای کوئریها و جهشهای GraphQL دارد.
- Relay: کلاینت GraphQL خود فیسبوک، که از ابتدا برای Suspense و Concurrent React طراحی شده است. به یک اسکیمای GraphQL خاص و یک مرحله کامپایل نیاز دارد اما عملکرد و سازگاری داده بینظیری ارائه میدهد.
- Urql: یک کلاینت GraphQL سبک و بسیار قابل تنظیم با پشتیبانی از 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 را با ارائه موارد زیر تکمیل میکنند:
- کش قوی: آنها یک کش پیچیده در حافظه از دادههای واکشی شده نگهداری میکنند، آن را در صورت در دسترس بودن فوراً ارائه میدهند و اعتبارسنجی مجدد در پسزمینه را مدیریت میکنند.
- بیاعتبار کردن و واکشی مجدد داده: آنها مکانیزمهایی برای علامتگذاری دادههای کش شده به عنوان «کهنه» و واکشی مجدد آنها ارائه میدهند (مثلاً، پس از یک جهش، یک تعامل کاربر، یا هنگام فوکوس روی پنجره).
- بهروزرسانیهای خوشبینانه: برای جهشها، به شما اجازه میدهند تا UI را بلافاصله (به صورت خوشبینانه) بر اساس نتیجه مورد انتظار یک فراخوانی API بهروز کنید و سپس در صورت شکست فراخوانی واقعی API، آن را برگردانید.
- همگامسازی وضعیت سراسری: آنها اطمینان حاصل میکنند که اگر دادهها از یک بخش از اپلیکیشن شما تغییر کنند، تمام کامپوننتهایی که آن دادهها را نمایش میدهند به طور خودکار بهروز میشوند.
- وضعیتهای بارگذاری و خطا برای جهشها: در حالی که
useQuery
ممکن است معلق شود،useMutation
معمولاً وضعیتهایisLoading
وisError
را برای خود فرآیند جهش فراهم میکند، زیرا جهشها اغلب تعاملی هستند و به بازخورد فوری نیاز دارند.
بدون یک کتابخانه واکشی داده قوی، پیادهسازی این ویژگیها بر روی یک مدیر منبع 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) یک تکامل قابل توجه را نشان میدهند و قول میدهند که مرزهای بین کلاینت و سرور را محو کرده و واکشی داده را بیشتر بهینه کنند.
- کامپوننتهای سرور ریاکت (RSC): این کامپوننتها روی سرور رندر میشوند، دادههای خود را مستقیماً واکشی میکنند و سپس فقط HTML و جاوااسکریپت سمت کلاینت لازم را به مرورگر ارسال میکنند. این کار آبشارهای سمت کلاینت را حذف میکند، اندازه باندلها را کاهش میدهد و عملکرد بارگذاری اولیه را بهبود میبخشد. RSCها با Suspense دست در دست هم کار میکنند: کامپوننتهای سرور میتوانند در صورت عدم آمادگی دادههایشان معلق شوند و سرور میتواند یک fallback Suspense را به صورت جریانی به کلاینت ارسال کند که سپس با حل شدن دادهها جایگزین میشود. این یک تغییردهنده بازی برای اپلیکیشنهایی با نیازهای دادهای پیچیده است و یک تجربه یکپارچه و بسیار کارآمد را ارائه میدهد، که به ویژه برای کاربران در مناطق جغرافیایی مختلف با تأخیرهای متفاوت مفید است.
- واکشی داده یکپارچه: چشمانداز بلندمدت برای ریاکت شامل یک رویکرد یکپارچه برای واکشی داده است، که در آن چارچوب اصلی یا راهحلهای کاملاً یکپارچه، پشتیبانی درجه یک برای بارگذاری داده هم در سرور و هم در کلاینت، که همگی توسط Suspense هماهنگ شدهاند، فراهم میکنند.
- تکامل مداوم کتابخانهها: کتابخانههای واکشی داده به تکامل خود ادامه خواهند داد و ویژگیهای پیچیدهتری برای کش، بیاعتبار کردن و بهروزرسانیهای بیدرنگ، بر پایه قابلیتهای بنیادی Suspense، ارائه خواهند داد.
همانطور که ریاکت به بلوغ خود ادامه میدهد، Suspense به طور فزایندهای به یک قطعه اصلی از پازل برای ساخت اپلیکیشنهای بسیار کارآمد، کاربرپسند و قابل نگهداری تبدیل خواهد شد. این توسعهدهندگان را به سمت یک روش اعلانیتر و انعطافپذیرتر برای مدیریت عملیات ناهمزمان سوق میدهد و پیچیدگی را از کامپوننتهای فردی به یک لایه داده به خوبی مدیریت شده منتقل میکند.
نتیجهگیری
React Suspense، که در ابتدا یک ویژگی برای تقسیم کد بود، به یک ابزار تحولآفرین برای واکشی داده تبدیل شده است. با پذیرش الگوی واکشی-همزمان-با-رندر و بهرهگیری از کتابخانههای آگاه از Suspense، توسعهدهندگان میتوانند تجربه کاربری اپلیکیشنهای خود را به طور قابل توجهی بهبود بخشند، آبشارهای بارگذاری را حذف کنند، منطق کامپوننت را ساده کنند و وضعیتهای بارگذاری روان و هماهنگ ارائه دهند. همراه با مرزهای خطا برای مدیریت خطای قوی و وعده آینده کامپوننتهای سرور ریاکت، Suspense ما را قادر میسازد تا اپلیکیشنهایی بسازیم که نه تنها کارآمد و انعطافپذیر هستند، بلکه ذاتاً برای کاربران در سراسر جهان لذتبخشترند. تغییر به یک پارادایم واکشی داده مبتنی بر Suspense نیاز به یک تعدیل مفهومی دارد، اما مزایای آن از نظر وضوح کد، عملکرد و رضایت کاربر قابل توجه و ارزش سرمایهگذاری را دارد.