استكشف React Suspense لجلب البيانات بما يتجاوز تقسيم الكود. افهم نمط 'الجلب أثناء العرض'، ومعالجة الأخطاء، والأنماط المستقبلية للتطبيقات العالمية.
تحميل الموارد باستخدام React Suspense: إتقان أنماط جلب البيانات الحديثة
في عالم تطوير الويب الديناميكي، تسود تجربة المستخدم (UX). من المتوقع أن تكون التطبيقات سريعة وسريعة الاستجابة وممتعة، بغض النظر عن ظروف الشبكة أو إمكانيات الجهاز. بالنسبة لمطوري React، غالبًا ما يترجم هذا إلى إدارة حالة معقدة، ومؤشرات تحميل مركبة، ومعركة مستمرة ضد شلالات جلب البيانات. هنا يأتي دور React Suspense، وهي ميزة قوية، وإن كانت غالبًا ما يُساء فهمها، مصممة لتغيير طريقة تعاملنا مع العمليات غير المتزامنة بشكل جذري، وخاصة جلب البيانات.
تم تقديم Suspense في البداية لتقسيم الكود (code splitting) مع React.lazy()
، لكن إمكاناته الحقيقية تكمن في قدرته على تنسيق تحميل *أي* مورد غير متزامن، بما في ذلك البيانات من واجهة برمجة التطبيقات (API). سيتعمق هذا الدليل الشامل في استخدام React Suspense لتحميل الموارد، مستكشفًا مفاهيمه الأساسية، وأنماط جلب البيانات الأساسية، والاعتبارات العملية لبناء تطبيقات عالمية عالية الأداء ومرنة.
تطور جلب البيانات في React: من الأسلوب الأمري إلى الأسلوب التعريفي
لسنوات عديدة، اعتمد جلب البيانات في مكونات React بشكل أساسي على نمط شائع: استخدام خطاف useEffect
لبدء استدعاء API، وإدارة حالات التحميل والخطأ باستخدام useState
، والعرض المشروط بناءً على هذه الحالات. وعلى الرغم من فعالية هذا النهج، إلا أنه غالبًا ما أدى إلى العديد من التحديات:
- انتشار حالة التحميل: احتاج كل مكون يتطلب بيانات تقريبًا إلى حالات
isLoading
وisError
وdata
الخاصة به، مما أدى إلى كود متكرر. - الشلالات وحالات التسابق: غالبًا ما أدت المكونات المتداخلة التي تجلب البيانات إلى طلبات متسلسلة (شلالات)، حيث يجلب المكون الأصل البيانات، ثم يعرضها، ثم يجلب المكون الفرعي بياناته، وهكذا. أدى هذا إلى زيادة أوقات التحميل الإجمالية. يمكن أن تحدث حالات التسابق أيضًا عند بدء طلبات متعددة، ووصول الردود بترتيب مختلف.
- معالجة الأخطاء المعقدة: قد يكون توزيع رسائل الخطأ ومنطق الاسترداد عبر العديد من المكونات مرهقًا، مما يتطلب تمرير الخصائص (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>Loading user profile...</p>;
}
if (error) {
return <p style={"color: red;"}>Error: {error.message}</p>;
}
if (!user) {
return <p>No user data available.</p>;
}
return (
<div>
<h2>User: {user.name}</h2>
<p>Email: {user.email}</p>
<!-- المزيد من تفاصيل المستخدم -->
</div>
);
}
function App() {
return (
<div>
<h1>Welcome to the Application</h1>
<UserProfile userId={"123"} />
</div>
);
}
هذا النمط منتشر في كل مكان، لكنه يجبر المكون على إدارة حالته غير المتزامنة، مما يؤدي غالبًا إلى علاقة وثيقة بين واجهة المستخدم ومنطق جلب البيانات. يقدم Suspense بديلاً أكثر تعريفية وانسيابية.
فهم React Suspense بما يتجاوز تقسيم الكود
يواجه معظم المطورين Suspense لأول مرة من خلال React.lazy()
لتقسيم الكود، حيث يسمح لك بتأجيل تحميل كود المكون حتى تكون هناك حاجة إليه. على سبيل المثال:
import React, { Suspense, lazy } from 'react';
const LazyComponent = lazy(() => import('./MyHeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading component...</div>}>
<LazyComponent />
</Suspense>
);
}
في هذا السيناريو، إذا لم يتم تحميل MyHeavyComponent
بعد، فإن حدود <Suspense>
ستلتقط الوعد (promise) الذي تم إلقاؤه بواسطة lazy()
وتعرض fallback
حتى يصبح كود المكون جاهزًا. الفكرة الرئيسية هنا هي أن Suspense يعمل عن طريق التقاط الوعود التي يتم إلقاؤها أثناء العرض.
هذه الآلية ليست حصرية لتحميل الكود. يمكن لأي دالة يتم استدعاؤها أثناء العرض والتي تلقي بوعد (على سبيل المثال، لأن موردًا غير متاح بعد) أن يتم التقاطها بواسطة حدود Suspense في مستوى أعلى في شجرة المكونات. عندما يتم حل الوعد، يحاول React إعادة عرض المكون، وإذا كان المورد متاحًا الآن، يتم إخفاء الـ fallback، ويتم عرض المحتوى الفعلي.
المفاهيم الأساسية لـ Suspense لجلب البيانات
للاستفادة من Suspense في جلب البيانات، نحتاج إلى فهم بعض المبادئ الأساسية:
1. إلقاء وعد (Throwing a Promise)
على عكس الكود غير المتزامن التقليدي الذي يستخدم async/await
لحل الوعود، يعتمد Suspense على دالة *تلقي* بوعد إذا لم تكن البيانات جاهزة. عندما يحاول React عرض مكون يستدعي مثل هذه الدالة، والبيانات لا تزال معلقة، يتم إلقاء الوعد. ثم يقوم React 'بإيقاف' عرض هذا المكون وأبنائه مؤقتًا، باحثًا عن أقرب حدود <Suspense>
.
2. حدود Suspense (The Suspense Boundary)
يعمل مكون <Suspense>
كحدود خطأ للوعود. يأخذ خاصية fallback
، وهي واجهة المستخدم التي يتم عرضها بينما يكون أي من أبنائه (أو أحفادهم) في حالة تعليق (أي، يلقي بوعد). بمجرد حل جميع الوعود التي تم إلقاؤها داخل شجرته الفرعية، يتم استبدال الـ fallback بالمحتوى الفعلي.
يمكن لحدود Suspense واحدة إدارة عمليات غير متزامنة متعددة. على سبيل المثال، إذا كان لديك مكونان داخل نفس حدود <Suspense>
، وكل منهما يحتاج إلى جلب بيانات، فسيتم عرض الـ fallback حتى يكتمل جلب البيانات *لكليهما*. هذا يتجنب إظهار واجهة مستخدم جزئية ويوفر تجربة تحميل أكثر تنسيقًا.
3. مدير ذاكرة التخزين المؤقت/الموارد (مسؤولية المطور)
بشكل حاسم، لا يتعامل Suspense نفسه مع جلب البيانات أو التخزين المؤقت. إنه مجرد آلية تنسيق. لجعل Suspense يعمل لجلب البيانات، تحتاج إلى طبقة تقوم بما يلي:
- بدء جلب البيانات.
- تخزين النتيجة مؤقتًا (بيانات تم حلها أو وعد معلق).
- توفير طريقة
read()
متزامنة إما أن تعيد البيانات المخزنة مؤقتًا على الفور (إذا كانت متوفرة) أو تلقي بالوعد المعلق (إذا لم تكن كذلك).
عادةً ما يتم تنفيذ 'مدير الموارد' هذا باستخدام ذاكرة تخزين مؤقت بسيطة (مثل Map أو كائن) لتخزين حالة كل مورد (معلق، تم حله، أو خطأ). بينما يمكنك بناء هذا يدويًا لأغراض العرض التوضيحي، في تطبيق واقعي، ستستخدم مكتبة جلب بيانات قوية تتكامل مع Suspense.
4. الوضع المتزامن (تحسينات React 18)
بينما يمكن استخدام Suspense في الإصدارات الأقدم من React، يتم إطلاق العنان لقوته الكاملة مع React المتزامن (Concurrent React) (ممكّن افتراضيًا في React 18 مع createRoot
). يسمح الوضع المتزامن لـ React بمقاطعة عمل العرض وإيقافه مؤقتًا واستئنافه. هذا يعني:
- تحديثات واجهة المستخدم غير الحاجبة: عندما يعرض Suspense واجهة احتياطية (fallback)، يمكن لـ React الاستمرار في عرض أجزاء أخرى من واجهة المستخدم غير المعلقة، أو حتى إعداد واجهة المستخدم الجديدة في الخلفية دون حجب الخيط الرئيسي.
- الانتقالات (Transitions): تسمح واجهات برمجة التطبيقات الجديدة مثل
useTransition
بتمييز تحديثات معينة على أنها 'انتقالات'، والتي يمكن لـ React مقاطعتها وجعلها أقل إلحاحًا، مما يوفر تغييرات أكثر سلاسة في واجهة المستخدم أثناء جلب البيانات.
أنماط جلب البيانات مع Suspense
دعنا نستكشف تطور أنماط جلب البيانات مع ظهور Suspense.
النمط 1: الجلب ثم العرض (التقليدي مع تغليف Suspense)
هذا هو النهج الكلاسيكي حيث يتم جلب البيانات، وبعد ذلك فقط يتم عرض المكون. على الرغم من عدم الاستفادة من آلية 'إلقاء الوعد' مباشرة للبيانات، يمكنك تغليف مكون يقوم *في النهاية* بعرض البيانات في حدود Suspense لتوفير واجهة احتياطية. يتعلق هذا الأمر باستخدام Suspense كمنسق لواجهة مستخدم التحميل العامة للمكونات التي تصبح جاهزة في النهاية، حتى لو كان جلب البيانات الداخلي لا يزال يعتمد على 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>Loading user details...</p>;
}
return (
<div>
<h3>User: {user.name}</h3>
<p>Email: {user.email}</p>
</div>
);
}
function App() {
return (
<div>
<h1>Fetch-Then-Render Example</h1>
<Suspense fallback={<div>Overall page loading...</div>}>
<UserDetails userId={"1"} />
</Suspense>
</div>
);
}
الإيجابيات: سهل الفهم، متوافق مع الإصدارات السابقة. يمكن استخدامه كطريقة سريعة لإضافة حالة تحميل عامة.
السلبيات: لا يزيل الكود المتكرر داخل UserDetails
. لا يزال عرضة للشلالات إذا كانت المكونات تجلب البيانات بشكل متسلسل. لا يستفيد حقًا من آلية 'الإلقاء والالتقاط' في Suspense للبيانات نفسها.
النمط 2: العرض ثم الجلب (الجلب داخل العرض، ليس للإنتاج)
هذا النمط هو في المقام الأول لتوضيح ما لا يجب فعله مع 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: {user.name}</h3>
<p>Email: {user.email}</p>
</div>
);
}
function App() {
return (
<div>
<h1>Render-Then-Fetch (Illustrative, NOT Recommended Directly)</h1>
<Suspense fallback={<div>Loading user...</div>}>
<UserDetailsBadExample userId={"2"} />
</Suspense>
</div>
);
}
الإيجابيات: يوضح كيف يمكن للمكون أن 'يطلب' البيانات مباشرة ويعلق إذا لم تكن جاهزة.
السلبيات: إشكالي للغاية للإنتاج. هذا النظام اليدوي والعام لـ fetchedData
و dataPromise
بسيط، ولا يتعامل مع طلبات متعددة، أو الإبطال، أو حالات الخطأ بقوة. إنه توضيح بدائي لمفهوم 'إلقاء الوعد'، وليس نمطًا يجب اعتماده.
النمط 3: الجلب أثناء العرض (Fetch-As-You-Render) (النمط المثالي لـ Suspense)
هذا هو التحول النموذجي الذي يتيحه Suspense حقًا لجلب البيانات. بدلاً من انتظار عرض المكون قبل جلب بياناته، أو جلب جميع البيانات مقدمًا، يعني نمط 'الجلب أثناء العرض' أنك تبدأ في جلب البيانات *في أقرب وقت ممكن*، غالبًا *قبل* أو *بالتزامن مع* عملية العرض. ثم تقوم المكونات 'بقراءة' البيانات من ذاكرة التخزين المؤقت، وإذا لم تكن البيانات جاهزة، فإنها تعلق. الفكرة الأساسية هي فصل منطق جلب البيانات عن منطق عرض المكون.
لتنفيذ 'الجلب أثناء العرض'، تحتاج إلى آلية من أجل:
- بدء جلب البيانات خارج دالة عرض المكون (على سبيل المثال، عند الدخول إلى مسار، أو النقر فوق زر).
- تخزين الوعد أو البيانات التي تم حلها في ذاكرة تخزين مؤقت.
- توفير طريقة للمكونات 'للقراءة' من ذاكرة التخزين المؤقت هذه. إذا لم تكن البيانات متاحة بعد، فإن دالة القراءة تلقي بالوعد المعلق.
يعالج هذا النمط مشكلة الشلال. إذا احتاج مكونان مختلفان إلى بيانات، يمكن بدء طلباتهما بالتوازي، وستظهر واجهة المستخدم فقط بمجرد أن يكون *كلاهما* جاهزًا، بتنسيق من حدود 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(`Fetching user ${id}...`);
return new Promise(resolve => setTimeout(() => {
const users = {
'1': { id: '1', name: 'Alice Smith', email: 'alice@example.com' },
'2': { id: '2', name: 'Bob Johnson', email: 'bob@example.com' },
'3': { id: '3', name: 'Charlie Brown', email: 'charlie@example.com' }
};
resolve(users[id]);
}, 1500));
};
const fetchPostsByUserId = (userId) => {
console.log(`Fetching posts for user ${userId}...`);
return new Promise(resolve => setTimeout(() => {
const posts = {
'1': [{ id: 'p1', title: 'My First Post' }, { id: 'p2', title: 'Travel Adventures' }],
'2': [{ id: 'p3', title: 'Coding Insights' }],
'3': [{ id: 'p4', title: 'Global Trends' }, { id: 'p5', title: 'Local Cuisine' }]
};
resolve(posts[userId] || []);
}, 2000));
};
// --- المكونات --- //
function UserProfile({ userId }) {
const userResource = fetchData(`user-${userId}`, () => fetchUserById(userId));
const user = userResource.read(); // سيعلق هذا إذا لم تكن بيانات المستخدم جاهزة
return (
<div>
<h3>User: {user.name}</h3>
<p>Email: {user.email}</p>
</div>
);
}
function UserPosts({ userId }) {
const postsResource = fetchData(`posts-${userId}`, () => fetchPostsByUserId(userId));
const posts = postsResource.read(); // سيعلق هذا إذا لم تكن بيانات المنشورات جاهزة
return (
<div>
<h4>Posts by {userId}:</h4>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
{posts.length === 0 && <li>No posts found.</li>}
</ul>
</div>
);
}
// --- التطبيق --- //
let initialUserResource = null;
let initialPostsResource = null;
function prefetchDataForUser(userId) {
initialUserResource = fetchData(`user-${userId}`, () => fetchUserById(userId));
initialPostsResource = fetchData(`posts-${userId}`, () => fetchPostsByUserId(userId));
}
// جلب بعض البيانات مسبقًا قبل حتى عرض مكون التطبيق
prefetchDataForUser('1');
function App() {
return (
<div>
<h1>Fetch-As-You-Render with Suspense</h1>
<p>This demonstrates how data fetching can happen in parallel, coordinated by Suspense.</p>
<Suspense fallback={<div>Loading user profile and posts...</div>}>
<UserProfile userId={"1"} />
<UserPosts userId={"1"} />
</Suspense>
<h2>Another Section</h2>
<Suspense fallback={<div>Loading different user...</div>}>
<UserProfile userId={"2"} />
</Suspense>
</div>
);
}
في هذا المثال:
- تقوم الدالتان
createResource
وfetchData
بإعداد آلية تخزين مؤقت أساسية. - عندما تستدعي
UserProfile
أوUserPosts
الدالةresource.read()
، فإنها إما تحصل على البيانات على الفور أو يتم إلقاء الوعد. - تلتقط أقرب حدود
<Suspense>
الوعد (أو الوعود) وتعرض الواجهة الاحتياطية الخاصة بها. - بشكل حاسم، يمكننا استدعاء
prefetchDataForUser('1')
*قبل* عرض مكونApp
، مما يسمح ببدء جلب البيانات في وقت مبكر.
مكتبات لنمط 'الجلب أثناء العرض'
إن بناء وصيانة مدير موارد قوي يدويًا أمر معقد. لحسن الحظ، اعتمدت العديد من مكتبات جلب البيانات الناضجة Suspense أو هي في طور اعتماده، مما يوفر حلولاً مجربة ومختبرة:
- React Query (TanStack Query): تقدم طبقة قوية لجلب البيانات والتخزين المؤقت مع دعم Suspense. توفر خطافات مثل
useQuery
يمكنها التعليق. إنها ممتازة لواجهات برمجة تطبيقات REST. - SWR (Stale-While-Revalidate): مكتبة جلب بيانات شائعة وخفيفة الوزن أخرى تدعم Suspense بالكامل. مثالية لواجهات برمجة تطبيقات REST، وتركز على توفير البيانات بسرعة (قديمة) ثم إعادة التحقق منها في الخلفية.
- Apollo Client: عميل GraphQL شامل لديه تكامل قوي مع Suspense لاستعلامات وتعديلات GraphQL.
- Relay: عميل GraphQL الخاص بفيسبوك، مصمم من الألف إلى الياء لـ Suspense و Concurrent React. يتطلب مخطط GraphQL معين وخطوة تجميع ولكنه يوفر أداءً واتساقًا للبيانات لا مثيل لهما.
- Urql: عميل GraphQL خفيف الوزن وقابل للتخصيص بدرجة عالية مع دعم Suspense.
تقوم هذه المكتبات بتجريد تعقيدات إنشاء وإدارة الموارد، ومعالجة التخزين المؤقت، وإعادة التحقق، والتحديثات المتفائلة، ومعالجة الأخطاء، مما يجعل تنفيذ 'الجلب أثناء العرض' أسهل بكثير.
النمط 4: الجلب المسبق (Prefetching) مع المكتبات المتوافقة مع Suspense
الجلب المسبق هو تحسين قوي حيث تقوم بشكل استباقي بجلب البيانات التي من المحتمل أن يحتاجها المستخدم في المستقبل القريب، حتى قبل أن يطلبها صراحة. يمكن أن يحسن هذا بشكل كبير الأداء المتصور.
مع المكتبات المتوافقة مع Suspense، يصبح الجلب المسبق سلسًا. يمكنك تشغيل عمليات جلب البيانات عند تفاعلات المستخدم التي لا تغير واجهة المستخدم على الفور، مثل تمرير مؤشر الماوس فوق رابط أو زر.
import React, { Suspense } from 'react';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
// افترض أن هذه هي استدعاءات API الخاصة بك
const fetchProductById = async (id) => {
console.log(`Fetching product ${id}...`);
return new Promise(resolve => setTimeout(() => {
const products = {
'A001': { id: 'A001', name: 'Global Widget X', price: 29.99, description: 'A versatile widget for international use.' },
'B002': { id: 'B002', name: 'Universal Gadget Y', price: 149.99, description: 'Cutting-edge gadget, loved worldwide.' },
};
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>Price: ${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(`Prefetching product ${productId}`);
};
return (
<div>
<h2>Available Products:</h2>
<ul>
<li>
<a href=\"#\" onMouseEnter={() => handleProductHover('A001')}
onClick={(e) => { e.preventDefault(); /* انتقل أو أظهر التفاصيل */ }}
>Global Widget X (A001)</a>
</li>
<li>
<a href=\"#\" onMouseEnter={() => handleProductHover('B002')}
onClick={(e) => { e.preventDefault(); /* انتقل أو أظهر التفاصيل */ }}
>Universal Gadget Y (B002)</a>
</li>
</ul>
<p>Hover over a product link to see prefetching in action. Open network tab to observe.</p>
</div>
);
}
function App() {
const [showProductA, setShowProductA] = React.useState(false);
const [showProductB, setShowProductB] = React.useState(false);
return (
<QueryClientProvider client={queryClient}>
<h1>Prefetching with React Suspense (React Query)</h1>
<ProductList />
<button onClick={() => setShowProductA(true)}>Show Global Widget X</button>
<button onClick={() => setShowProductB(true)}>Show Universal Gadget Y</button>
{showProductA && (
<Suspense fallback={<p>Loading Global Widget X...</p>}>
<ProductDetails productId=\"A001\" />
</Suspense>
)}
{showProductB && (
<Suspense fallback={<p>Loading Universal Gadget Y...</p>}>
<ProductDetails productId=\"B002\" />
</Suspense>
)}
</QueryClientProvider>
);
}
في هذا المثال، يؤدي تمرير مؤشر الماوس فوق رابط المنتج إلى تشغيل `queryClient.prefetchQuery`، الذي يبدأ جلب البيانات في الخلفية. إذا قام المستخدم بعد ذلك بالنقر فوق الزر لإظهار تفاصيل المنتج، وكانت البيانات موجودة بالفعل في ذاكرة التخزين المؤقت من الجلب المسبق، فسيتم عرض المكون على الفور دون تعليق. إذا كان الجلب المسبق لا يزال قيد التقدم أو لم يتم بدؤه، فسيعرض Suspense الواجهة الاحتياطية حتى تصبح البيانات جاهزة.
معالجة الأخطاء مع Suspense وحدود الأخطاء (Error Boundaries)
بينما يتعامل Suspense مع حالة 'التحميل' عن طريق عرض واجهة احتياطية، فإنه لا يتعامل مباشرة مع حالات 'الخطأ'. إذا تم رفض الوعد الذي ألقاه مكون معلق (أي، فشل جلب البيانات)، فسينتشر هذا الخطأ لأعلى في شجرة المكونات. للتعامل مع هذه الأخطاء بأمان وعرض واجهة مستخدم مناسبة، تحتاج إلى استخدام حدود الأخطاء (Error Boundaries).
حدود الخطأ هو مكون React يطبق إحدى دورتي الحياة componentDidCatch
أو static getDerivedStateFromError
. يلتقط أخطاء JavaScript في أي مكان في شجرة المكونات الفرعية الخاصة به، بما في ذلك الأخطاء التي تلقيها الوعود التي كان من الممكن أن يلتقطها 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) {
// تحديث الحالة حتى يظهر العرض التالي واجهة المستخدم الاحتياطية.
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// يمكنك أيضًا تسجيل الخطأ في خدمة تقارير الأخطاء
console.error(\"Caught an error:\", error, errorInfo);
}
render() {
if (this.state.hasError) {
// يمكنك عرض أي واجهة مستخدم احتياطية مخصصة
return (
<div style={{\"border\": \"2px solid red\", \"padding\": \"20px\", \"margin\": \"20px 0\", \"background\": \"#ffe0e0\"}}>
<h2>Something went wrong!</h2>
<p>{this.state.error && this.state.error.message}</p>
<p>Please try refreshing the page or contact support.</p>
<button onClick={() => this.setState({ hasError: false, error: null })}>Try Again</button>
</div>
);
}
return this.props.children;
}
}
// --- جلب البيانات (مع احتمال حدوث خطأ) --- //
const fetchItemById = async (id) => {
console.log(`Attempting to fetch item ${id}...`);
return new Promise((resolve, reject) => setTimeout(() => {
if (id === 'error-item') {
reject(new Error('Failed to load item: Network unreachable or item not found.'));
} else if (id === 'slow-item') {
resolve({ id: 'slow-item', name: 'Delivered Slowly', data: 'This item took a while but arrived!', status: 'success' });
} else {
resolve({ id, name: `Item ${id}`, data: `Data for item ${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>Item Details:</h3>
<p>ID: {item.id}</p>
<p>Name: {item.name}</p>
<p>Data: {item.data}</p>
</div>
);
}
function App() {
const [fetchType, setFetchType] = useState('normal-item');
return (
<QueryClientProvider client={queryClient}>
<h1>Suspense and Error Boundaries</h1>
<div>
<button onClick={() => setFetchType('normal-item')}>Fetch Normal Item</button>
<button onClick={() => setFetchType('slow-item')}>Fetch Slow Item</button>
<button onClick={() => setFetchType('error-item')}>Fetch Error Item</button>
</div>
<MyErrorBoundary>
<Suspense fallback={<p>Loading item via Suspense...</p>}>
<DisplayItem itemId={fetchType} />
</Suspense>
</MyErrorBoundary>
</QueryClientProvider>
);
}
من خلال تغليف حدود Suspense (أو المكونات التي قد تعلق) بحدود خطأ، فإنك تضمن التقاط فشل الشبكة أو أخطاء الخادم أثناء جلب البيانات والتعامل معها بأمان، مما يمنع تعطل التطبيق بأكمله. يوفر هذا تجربة قوية وسهلة الاستخدام، مما يسمح للمستخدمين بفهم المشكلة وإمكانية إعادة المحاولة.
إدارة الحالة وإبطال البيانات مع Suspense
من المهم توضيح أن React Suspense يعالج بشكل أساسي حالة التحميل الأولية للموارد غير المتزامنة. لا يدير بطبيعته ذاكرة التخزين المؤقت من جانب العميل، أو يتعامل مع إبطال البيانات، أو ينسق التعديلات (عمليات الإنشاء والتحديث والحذف) وتحديثات واجهة المستخدم اللاحقة.
هنا تصبح مكتبات جلب البيانات المتوافقة مع Suspense (React Query, SWR, Apollo Client, Relay) لا غنى عنها. إنها تكمل Suspense من خلال توفير:
- التخزين المؤقت القوي: تحافظ على ذاكرة تخزين مؤقت متطورة للبيانات التي تم جلبها، وتخدمها على الفور إذا كانت متاحة، وتتعامل مع إعادة التحقق في الخلفية.
- إبطال البيانات وإعادة الجلب: توفر آليات لتمييز البيانات المخزنة مؤقتًا على أنها 'قديمة' وإعادة جلبها (على سبيل المثال، بعد تعديل، أو تفاعل مستخدم، أو عند التركيز على النافذة).
- التحديثات المتفائلة: بالنسبة للتعديلات، تسمح لك بتحديث واجهة المستخدم على الفور (بتفاؤل) بناءً على النتيجة المتوقعة لاستدعاء API، ثم التراجع إذا فشل استدعاء API الفعلي.
- مزامنة الحالة العامة: تضمن أنه إذا تغيرت البيانات من جزء واحد من تطبيقك، يتم تحديث جميع المكونات التي تعرض تلك البيانات تلقائيًا.
- حالات التحميل والخطأ للتعديلات: بينما قد يعلق `useQuery`، يوفر `useMutation` عادةً حالتي `isLoading` و `isError` لعملية التعديل نفسها، حيث أن التعديلات غالبًا ما تكون تفاعلية وتتطلب ملاحظات فورية.
بدون مكتبة جلب بيانات قوية، سيكون تنفيذ هذه الميزات فوق مدير موارد Suspense يدوي مهمة كبيرة، تتطلب منك بشكل أساسي بناء إطار عمل جلب البيانات الخاص بك.
الاعتبارات العملية وأفضل الممارسات
يعد اعتماد Suspense لجلب البيانات قرارًا معماريًا مهمًا. فيما يلي بعض الاعتبارات العملية لتطبيق عالمي:
1. ليست كل البيانات بحاجة إلى Suspense
يعتبر Suspense مثاليًا للبيانات الهامة التي تؤثر بشكل مباشر على العرض الأولي للمكون. بالنسبة للبيانات غير الهامة، أو عمليات الجلب في الخلفية، أو البيانات التي يمكن تحميلها ببطء دون تأثير بصري قوي، قد يظل useEffect
التقليدي أو العرض المسبق مناسبًا. يمكن أن يؤدي الإفراط في استخدام Suspense إلى تجربة تحميل أقل دقة، حيث تنتظر حدود Suspense واحدة حل *جميع* أبنائها.
2. دقة حدود Suspense
ضع حدود <Suspense>
الخاصة بك بعناية. قد تخفي حدود واحدة كبيرة في الجزء العلوي من تطبيقك الصفحة بأكملها خلف مؤشر تحميل، وهو ما قد يكون محبطًا. تسمح الحدود الأصغر والأكثر دقة لأجزاء مختلفة من صفحتك بالتحميل بشكل مستقل، مما يوفر تجربة أكثر تقدمًا واستجابة. على سبيل المثال، حدود حول مكون ملف تعريف المستخدم، وأخرى حول قائمة بالمنتجات الموصى بها.
<div>
<h1>Product Page</h1>
<Suspense fallback={<p>Loading main product details...</p>}>
<ProductDetails id=\"prod123\" />
</Suspense>
<hr />
<h2>Related Products</h2>
<Suspense fallback={<p>Loading related products...</p>}>
<RelatedProducts category=\"electronics\" />
</Suspense>
</div>
يعني هذا النهج أن المستخدمين يمكنهم رؤية تفاصيل المنتج الرئيسية حتى لو كانت المنتجات ذات الصلة لا تزال قيد التحميل.
3. العرض من جانب الخادم (SSR) وتدفق HTML
تتكامل واجهات برمجة تطبيقات العرض من جانب الخادم (SSR) الجديدة في React 18 (renderToPipeableStream
) تمامًا مع Suspense. يسمح هذا لخادمك بإرسال HTML بمجرد أن يكون جاهزًا، حتى لو كانت أجزاء من الصفحة (مثل المكونات المعتمدة على البيانات) لا تزال قيد التحميل. يمكن للخادم بث عنصر نائب (من واجهة Suspense الاحتياطية) ثم بث المحتوى الفعلي عند حل البيانات، دون الحاجة إلى إعادة عرض كاملة من جانب العميل. يؤدي هذا إلى تحسين أداء التحميل المتصور بشكل كبير للمستخدمين العالميين على ظروف الشبكة المتغيرة.
4. التبني التدريجي
لست بحاجة إلى إعادة كتابة تطبيقك بالكامل لاستخدام Suspense. يمكنك تقديمه تدريجيًا، بدءًا من الميزات أو المكونات الجديدة التي ستستفيد أكثر من أنماط التحميل التعريفية الخاصة به.
5. الأدوات وتصحيح الأخطاء
بينما يبسط Suspense منطق المكونات، يمكن أن يكون تصحيح الأخطاء مختلفًا. توفر أدوات مطوري React (React DevTools) رؤى حول حدود Suspense وحالاتها. تعرف على كيفية كشف مكتبة جلب البيانات التي اخترتها لحالتها الداخلية (على سبيل المثال، React Query Devtools).
6. مهلات لواجهات Suspense الاحتياطية
لأوقات التحميل الطويلة جدًا، قد ترغب في إدخال مهلة زمنية لواجهة Suspense الاحتياطية، أو التبديل إلى مؤشر تحميل أكثر تفصيلاً بعد تأخير معين. يمكن أن تساعد خطافات useDeferredValue
و useTransition
في React 18 في إدارة حالات التحميل الأكثر دقة هذه، مما يسمح لك بعرض إصدار 'قديم' من واجهة المستخدم أثناء جلب بيانات جديدة، أو تأجيل التحديثات غير العاجلة.
مستقبل جلب البيانات في React: مكونات خادم React وما بعدها
لا تتوقف رحلة جلب البيانات في React مع Suspense من جانب العميل. تمثل مكونات خادم React (RSC) تطورًا كبيرًا، وتعد بطمس الخطوط الفاصلة بين العميل والخادم، وتحسين جلب البيانات بشكل أكبر.
- مكونات خادم React (RSC): يتم عرض هذه المكونات على الخادم، وتجلب بياناتها مباشرة، ثم ترسل فقط HTML و JavaScript الضروريين من جانب العميل إلى المتصفح. يؤدي هذا إلى القضاء على شلالات جانب العميل، وتقليل أحجام الحزم، وتحسين أداء التحميل الأولي. تعمل RSC جنبًا إلى جنب مع Suspense: يمكن لمكونات الخادم التعليق إذا لم تكن بياناتها جاهزة، ويمكن للخادم بث واجهة Suspense احتياطية إلى العميل، والتي يتم استبدالها بعد ذلك عند حل البيانات. هذا يغير قواعد اللعبة للتطبيقات ذات متطلبات البيانات المعقدة، ويقدم تجربة سلسة وعالية الأداء، وهو مفيد بشكل خاص للمستخدمين عبر المناطق الجغرافية المختلفة ذات زمن الوصول المتفاوت.
- جلب البيانات الموحد: تتضمن الرؤية طويلة المدى لـ React نهجًا موحدًا لجلب البيانات، حيث يوفر إطار العمل الأساسي أو الحلول المتكاملة بشكل وثيق دعمًا من الدرجة الأولى لتحميل البيانات على كل من الخادم والعميل، وكل ذلك بتنسيق من Suspense.
- التطور المستمر للمكتبات: ستستمر مكتبات جلب البيانات في التطور، وتقدم ميزات أكثر تطورًا للتخزين المؤقت، والإبطال، والتحديثات في الوقت الفعلي، بناءً على القدرات الأساسية لـ Suspense.
مع استمرار نضج React، سيكون Suspense جزءًا مركزيًا بشكل متزايد من اللغز لبناء تطبيقات عالية الأداء وسهلة الاستخدام وقابلة للصيانة. إنه يدفع المطورين نحو طريقة أكثر تعريفية ومرونة للتعامل مع العمليات غير المتزامنة، ونقل التعقيد من المكونات الفردية إلى طبقة بيانات مُدارة جيدًا.
الخاتمة
React Suspense، الذي كان في البداية ميزة لتقسيم الكود، قد ازدهر ليصبح أداة تحويلية لجلب البيانات. من خلال تبني نمط 'الجلب أثناء العرض' والاستفادة من المكتبات المتوافقة مع Suspense، يمكن للمطورين تحسين تجربة المستخدم لتطبيقاتهم بشكل كبير، والقضاء على شلالات التحميل، وتبسيط منطق المكونات، وتوفير حالات تحميل سلسة ومنسقة. جنبًا إلى جنب مع حدود الأخطاء لمعالجة الأخطاء القوية والوعد المستقبلي لمكونات خادم React، يمكّننا Suspense من بناء تطبيقات ليست فقط عالية الأداء ومرنة ولكنها أيضًا أكثر إمتاعًا للمستخدمين في جميع أنحاء العالم. يتطلب التحول إلى نموذج جلب البيانات القائم على Suspense تعديلًا مفاهيميًا، لكن الفوائد من حيث وضوح الكود والأداء ورضا المستخدم كبيرة وتستحق الاستثمار.