جلب البيانات من السيرفر (Data Fetching) وإدارة حالات التحميل والخطأ
| الحالة البرمجية | دورها في دورة حياة المكون | الأثر المباشر على واجهة المستخدم (UX) |
|---|---|---|
| Data State | تخزين الاستجابة الناجحة القادمة من السيرفر (API Response) | عرض قائمة الكورسات أو المحتوى التفاعلي فور اكتمال وصوله |
| Loading State | مؤشر بولياني (Boolean) يحدد ما إذا كان الطلب جارياً في الخلفية | عرض مؤشر تحميل (Spinner/Skeleton) لمنع ارتباك المستخدم أو التفاعل المسبق |
| Error State | تخزين رسالة الخطأ في حال فشل الاتصال أو إرجاع السيرفر لكود خطأ | إظهار تنبيه أو زر إعادة المحاولة (Retry) بدلاً من انهيار التطبيق |
| Abort Signal | آلية لإيقاف طلب الـ HTTP الملغى عند خروج المستخدم من الصفحة | منع تسريب الذاكرة وتحديث حالة مكون غير موجود في الشاشة |
جلب البيانات عبر الشبكة (Async Data Fetching) هو أحد أبرز التأثيرات الجانبية (Side Effects) في تطبيقات الويب. نظراً لأن الاتصال بالسيرفر يستغرق وقتاً وغير مضمون النتيجة دائماً، يجب تصميم واجهة المستخدم لنتعامل مع 3 حالات متغيرة أساسية:
نحتاج إلى 3 حالات داخل المكون للسيطرة الكاملة على الشاشة:
| 1 | function CourseList() { |
| 2 | const [courses, setCourses] = useState([]); // 1. البيانات |
| 3 | const [isLoading, setIsLoading] = useState(true); // 2. جاري التحميل |
| 4 | const [error, setError] = useState(null); // 3. الخطأ |
| 5 | |
| 6 | useEffect(() => { |
| 7 | // جلب البيانات من الـ API |
| 8 | fetch('https://api.nouvil.net/courses') |
| 9 | .then((res) => { |
| 10 | if (!res.ok) throw new Error('فشل في جلب قائمة الكورسات'); |
| 11 | return res.json(); |
| 12 | }) |
| 13 | .then((data) => { |
| 14 | setCourses(data); |
| 15 | setIsLoading(false); // إيقاف التحميل عند النجاح |
| 16 | }) |
| 17 | .catch((err) => { |
| 18 | setError(err.message); |
| 19 | setIsLoading(false); // إيقاف التحميل حتى عند الفشل |
| 20 | }); |
| 21 | }, []); |
| 22 | |
| 23 | // التعامل مع الواجهة بحسب الحالة الحالية |
| 24 | if (isLoading) return <div>جاري تحميل الكورسات...</div>; |
| 25 | if (error) return <div>حدث خطأ: {error}</div>; |
| 26 | |
| 27 | return ( |
| 28 | <ul> |
| 29 | {courses.map((course) => ( |
| 30 | <li key={course.id}>{course.title}</li> |
| 31 | ))} |
| 32 | </ul> |
| 33 | ); |
| 34 | } |
دالة useEffect نفسها لا يمكن أن تكون async مباشرة! لأن دالة الـ Effect يجب أن ترجع دالة تنظيف (Cleanup Function) فقط أو لا ترجع شيئاً.
الطريقة الخاطئة:
| 1 | // خطأ قاتل! لا تجعل دالة useEffect من نوع async |
| 2 | useEffect(async () => { |
| 3 | const res = await fetch('...'); |
| 4 | }, []); |
الطريقة الصحيحة
| 1 | useEffect(() => { |
| 2 | const fetchCourses = async () => { |
| 3 | try { |
| 4 | const res = await fetch('https://api.nouvil.net/courses'); |
| 5 | const data = await res.json(); |
| 6 | setCourses(data); |
| 7 | } catch (err) { |
| 8 | setError(err.message); |
| 9 | } finally { |
| 10 | setIsLoading(false); // يُنفذ دائماً سواء نجح أو فشل |
| 11 | } |
| 12 | }; |
| 13 | |
| 14 | fetchCourses(); |
| 15 | }, []); |
إذا قام المستخدم بالتنقل بين الصفحات بسرعة قبل أن يكتمل طلب الـ API، سيحاول الكود التعديل على useState لمكون تم تدميره وإزالته من الـ DOM (Unmounted Component). لحل هذه المشكلة هندسياً، نستخدم AbortController لإلغاء طلب الشبكة داخل دالة التنظيف (Cleanup Function):
AbortController
| 1 | useEffect(() => { |
| 2 | const controller = new AbortController(); |
| 3 | |
| 4 | async function loadData() { |
| 5 | try { |
| 6 | const res = await fetch('https://api.nouvil.net/courses', { |
| 7 | signal: controller.signal // ربط الطلب بـ AbortSignal |
| 8 | }); |
| 9 | const data = await res.json(); |
| 10 | setCourses(data); |
| 11 | } catch (err) { |
| 12 | if (err.name !== 'AbortError') { |
| 13 | setError(err.message); // إهمال الخطأ إذا كان سببه إلغاء الطلب يدوياً |
| 14 | } |
| 15 | } |
| 16 | } |
| 17 | |
| 18 | loadData(); |
| 19 | |
| 20 | // إيقاف الطلب فوراً إذا خرج المستخدم من الصفحة قبل اكتماله |
| 21 | return () => controller.abort(); |
| 22 | }, []); |
ℹ️ معلومة
إذا كان لديك زر مثل "إعادة المحاولة" أو استعلام يعتمد على متغير مثل برامتر البحث [searchQuery]، تذكر دائماً تصفير حالة الخطأ وإعادة حالة التحميل للوضع المباشر قبل إطلاق الطلب الجديد:
| 1 | const handleRetry = () => { |
| 2 | setError(null); // 1. مسح الخطأ القديم |
| 3 | setIsLoading(true); // 2. تفعيل مؤشر التحميل |
| 4 | // 3. إعادة استدعاء دالة الجلب... |
| 5 | }; |