الوحدة 4 · البيانات والجهاز
جلب البيانات: حالات التحميل والخطأ والفراغ
20 دقراءة 3 دبيئة تجريب الكود
الدرس في لمحة
حمّل كتالوج الدروب في Trailhead من خادم باستخدام fetch، وصمّم حالات التحميل والخطأ والفراغ والنجاح صراحةً، وألغِ الطلبات القديمة، وتعامل مع الشبكات المتقلّبة التي تعيش عليها الهواتف.
- 1أربع حالات، وقيمة واحدة
- 2الجلب مع التنظيف
- 3السحب للتحديث والشبكات البطيئة
- 4غياب الاتصال حالة طبيعية، لا خطأ
- 5متى تستخدم مكتبة للبيانات
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
حتى الآن، عاشت بيانات Trailhead على الهاتف. أما الميزة التالية، كتالوج الدروب القريبة منك، فتأتي من خادم. على حاسوب محمول متصل بشبكة Wi-Fi المكتب، الطلب مجرّد إجراء شكلي. أما على هاتف عند بداية درب وفيه خطّ إشارة واحد، فهو أبطأ ما يفعله تطبيقك وأقلّه موثوقية: تستغرق الطلبات ثواني، أو تفشل في منتصفها، أو لا تعود أبدًا. والتصميم لهذا الواقع هو معظم العمل.
أربع حالات، وقيمة واحدة
كل شاشة تحمّل بيانات لها أربع حالات على الأقل يمكن أن يراها المستخدم: التحميل (loading)، والخطأ (error)، والفراغ (empty: اكتمل التحميل ولا يوجد شيء)، والنجاح (success). مثّلها بقيمة واحدة، فلا يمكن أن تحدث تركيبات مستحيلة:
export type Trail = { id: string; name: string; lengthKm: number; difficulty: 'easy' | 'moderate' | 'hard' };
export type Load<T> =
| { status: 'loading' }
| { status: 'error'; message: string }
| { status: 'success'; data: T };الفراغ ليس حالة منفصلة؛ إنه نجاح بصفر عناصر، والشاشة تقرّر كيف تعرضه.
الجلب مع التنظيف
import { useCallback, useEffect, useState } from 'react';
import { ActivityIndicator, FlatList, Pressable, Text, View } from 'react-native';
import type { Load, Trail } from '../../lib/types';
const API = process.env.EXPO_PUBLIC_API_URL;
async function getTrails(signal: AbortSignal): Promise<Trail[]> {
const res = await fetch(`${API}/trails?near=current`, { signal });
if (!res.ok) throw new Error(`Server responded ${res.status}`);
return res.json();
}
export default function Explore() {
const [state, setState] = useState<Load<Trail[]>>({ status: 'loading' });
const [attempt, setAttempt] = useState(0);
const retry = useCallback(() => setAttempt((n) => n + 1), []);
useEffect(() => {
const controller = new AbortController();
setState({ status: 'loading' });
getTrails(controller.signal)
.then((data) => setState({ status: 'success', data }))
.catch(() => {
if (controller.signal.aborted) return;
setState({ status: 'error', message: 'Could not load trails. Check your connection and try again.' });
});
return () => controller.abort();
}, [attempt]);
if (state.status === 'loading') return <ActivityIndicator style={{ flex: 1 }} size="large" />;
if (state.status === 'error') {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', gap: 12, padding: 24 }}>
<Text style={{ textAlign: 'center' }}>{state.message}</Text>
<Pressable onPress={retry} accessibilityRole="button"><Text>Try again</Text></Pressable>
</View>
);
}
return (
<FlatList
data={state.data}
keyExtractor={(t) => t.id}
renderItem={({ item }) => <Text style={{ padding: 16 }}>{item.name} · {item.lengthKm} km</Text>}
ListEmptyComponent={<Text style={{ padding: 24 }}>No trails within 50 km. Try a wider search.</Text>}
/>
);
}ثلاثة تفاصيل تحمل الثقل. يُفحص res.ok يدويًا، لأن fetch يعامل الـ 500 على أنه توصيل ناجح لأخبار سيئة. ويلغي الـ AbortController الطلب في دالة التنظيف، فمغادرة الشاشة (أو إعادة المحاولة) لا تسمح أبدًا لاستجابة قديمة بالكتابة فوق استجابة حديثة. ورسالة الخطأ تخبر المستخدم بما يفعل، بدلًا من طباعة TypeError: Network request failed.
يأتي EXPO_PUBLIC_API_URL من ملف .env. المتغيّرات التي تبدأ بـ EXPO_PUBLIC_ تُدرج مباشرة في حزمة JavaScript، وهذا يعني أيضًا أنها قابلة للقراءة من أي أحد يفكّ حزمة تطبيقك. ضع فيها العناوين، ولا تضع المفاتيح السرّية أبدًا.
السحب للتحديث والشبكات البطيئة
يتوقّع المستخدمون أن يسحبوا القائمة إلى الأسفل لإعادة تحميلها. يدعم FlatList ذلك بخاصّيتين:
<FlatList
// …
refreshing={refreshing}
onRefresh={async () => {
setRefreshing(true);
try { setState({ status: 'success', data: await getTrails(new AbortController().signal) }); }
catch { /* keep the old list and show a short "Couldn't refresh" message */ }
finally { setRefreshing(false); }
}}
/>يُبقي التحديث القائمة القديمة ظاهرة أثناء التحميل، وهذا ألطف من مؤشّر التحميل بملء الشاشة في التحميل الأول. وإن فشل التحديث، فاحتفظ بالبيانات القديمة واعرض رسالة قصيرة، بدلًا من استبدال قائمة سليمة تمامًا بشاشة خطأ.
والشبكات المتقلّبة حجّة أيضًا لصالح إعادة المحاولة التلقائية مع تأخير يتزايد بين المحاولات (ثانية، ثم ثانيتان، ثم أربع)، فيتعافى التطبيق وحده من انقطاع قصير في الإشارة. لا تُعِد المحاولة إلا لما يمكن أن ينجح لاحقًا: انتهاء المهلة أو 503 قد ينجحان، أما 404 أو 401 فلن ينجحا، لذا فالفشل السريع ألطف. ستكتب هذه الدالة المساعدة في التمرين.
غياب الاتصال حالة طبيعية، لا خطأ
في تطبيق للمشي الجبلي، غياب الإشارة هو الوضع المتوقّع لساعات متواصلة. تعامل معه على هذا الأساس. اعرض آخر قائمة حمّلتها، مع شريط هادئ يذكر وقت آخر تحديث، بدلًا من شاشة خطأ. ودع المستخدمين يواصلون تسجيل الرحلات، واحفظها على الجهاز، وأرسلها حين يعود الاتصال. تخبرك حزمة المجتمع @react-native-community/netinfo هل الجهاز متصل، وتُعلمك حين يتغيّر ذلك، وتلك هي اللحظة لإعادة محاولة الأعمال المؤجّلة.
لا شيء من ذلك يحتاج إلى أن يكون ذكيًا من اليوم الأول. المهم هو القرار: لكل شاشة، ماذا يرى المستخدم دون شبكة؟ اكتب الإجابة قبل أن تكتب الـ fetch، وصمّم حالات الفراغ والخطأ وغياب الاتصال بالعناية نفسها التي تصمّم بها المسار السعيد.
متى تستخدم مكتبة للبيانات
الكود أعلاه جيد لشاشة واحدة. أما في تطبيق حقيقي فستكرّره في كل مكان، وسيبقى ينقصك التخزين المؤقت (عُد إلى شاشة فترى البيانات فورًا)، ومنع تكرار الطلبات، وإعادة الجلب في الخلفية، وإعادة المحاولة. تتولّى TanStack Query كل ذلك عبر useQuery، وتتضمّن وثائقها الإعداد الصغير الخاص بـ React Native الذي يعيد الجلب حين يعود التطبيق إلى الواجهة. تعلّم النسخة اليدوية أولًا لتعرف ما تفعله المكتبة، ثم استخدم المكتبة في بيئة الإنتاج.
التالي: بيانات يجب أن تصمد دون أي شبكة على الإطلاق، مخزّنة على الجهاز.
أهم النقاط
- مثّل الطلب بقيمة حالة واحدة (loading أو error أو success) بدلًا من عدّة قيم منطقية قد يناقض بعضها بعضًا.
fetchلا يرفض (reject) إلا عند فشل الشبكة؛ تحقّق منresponse.okبنفسك لاستجابات 404 و500.- ألغِ الطلبات الجارية في دالة التنظيف الخاصة بالـ effect، لأن المستخدمين يغادرون الشاشات قبل أن تنتهي الطلبات البطيئة.
- كل قائمة تحمّل بيانات تحتاج إلى أربع حالات مصمّمة، منها حالة فراغ وحالة خطأ مع زرّ لإعادة المحاولة.
- على الهاتف،
localhostهو الهاتف نفسه؛ وجّه الـ development builds إلى عنوان حاسوبك على الشبكة أو إلى API منشور.