أساسيات React.js

أنماط جلب البيانات

18 دقيقة الدرس 24 من 40

أنماط جلب البيانات

تستخدم تطبيقات React الحديثة أنماط ومكتبات جلب بيانات متنوعة. من fetch API الأصلي إلى أدوات متخصصة مثل React Query و SWR، يعتمد اختيار النهج الصحيح على احتياجات تطبيقك للتخزين المؤقت، وإعادة التحقق، والتحديثات في الوقت الفعلي.

Fetch API الأصلي

النهج الأساسي باستخدام دالة fetch المدمجة في JavaScript:

// جلب أساسي مع hooks import { useState, useEffect } from 'react'; function useUser(userId) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; const fetchUser = async () => { try { setLoading(true); setError(null); const response = await fetch( `https://api.example.com/users/${userId}` ); if (!response.ok) { throw new Error(`خطأ HTTP! الحالة: ${response.status}`); } const data = await response.json(); if (!cancelled) { setUser(data); } } catch (err) { if (!cancelled) { setError(err.message); } } finally { if (!cancelled) { setLoading(false); } } }; fetchUser(); // دالة التنظيف لمنع تحديثات الحالة بعد إزالة التركيب return () => { cancelled = true; }; }, [userId]); return { user, loading, error }; } // الاستخدام function UserProfile({ userId }) { const { user, loading, error } = useUser(userId); if (loading) return <div>جاري التحميل...</div>; if (error) return <div>خطأ: {error}</div>; if (!user) return <div>لم يتم العثور على مستخدم</div>; return ( <div> <h2>{user.name}</h2> <p>{user.email}</p> </div> ); }
قيود Fetch API:
  • لا يوجد تخزين مؤقت أو إعادة تحقق تلقائي
  • يتطلب تنظيف يدوي لمنع تسرب الذاكرة
  • يجب التعامل مع حالات التحميل والخطأ يدويًا
  • لا يوجد إلغاء تكرار الطلبات مدمج
  • كود زائد يتكرر عبر المكونات

Axios - عميل HTTP محسّن

يوفر Axios واجهة برمجة تطبيقات أكثر قوة مع interceptors ومعالجة أخطاء أفضل:

// التثبيت: npm install axios import axios from 'axios'; import { useState, useEffect } from 'react'; // إنشاء نسخة axios مع الإعدادات الافتراضية const api = axios.create({ baseURL: 'https://api.example.com', timeout: 10000, headers: { 'Content-Type': 'application/json' } }); // interceptor الطلب - إضافة رمز المصادقة api.interceptors.request.use( (config) => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error) ); // interceptor الاستجابة - معالجة الأخطاء عالميًا api.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { // إعادة التوجيه لتسجيل الدخول window.location.href = '/login'; } return Promise.reject(error); } ); // Hook مخصص مع axios function useAxiosFetch(url, options = {}) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const source = axios.CancelToken.source(); const fetchData = async () => { try { setLoading(true); setError(null); const response = await api.get(url, { ...options, cancelToken: source.token }); setData(response.data); } catch (err) { if (!axios.isCancel(err)) { setError(err.message); } } finally { setLoading(false); } }; fetchData(); return () => { source.cancel('تم إزالة تركيب المكون'); }; }, [url]); return { data, loading, error }; } // الاستخدام function Products() { const { data, loading, error } = useAxiosFetch('/products'); if (loading) return <div>جاري تحميل المنتجات...</div>; if (error) return <div>خطأ: {error}</div>; return ( <ul> {data?.map(product => ( <li key={product.id}>{product.name}</li> ))} </ul> ); }
فوائد Axios: تحويل JSON تلقائي، interceptors للطلب/الاستجابة، إلغاء الطلبات، دعم المهلة، حماية CSRF، ومعالجة أخطاء أفضل. إنه أكثر غنى بالميزات من fetch لكنه يضيف تبعية لمشروعك.

SWR - Stale-While-Revalidate

SWR هي مكتبة React Hooks لجلب البيانات مع التخزين المؤقت وإعادة التحقق التلقائية:

// التثبيت: npm install swr import useSWR from 'swr'; // دالة الجالب - يمكن تخصيصها const fetcher = (url) => fetch(url).then(res => res.json()); // الاستخدام الأساسي function Profile() { const { data, error, isLoading } = useSWR( '/api/user', fetcher ); if (isLoading) return <div>جاري التحميل...</div>; if (error) return <div>فشل التحميل</div>; return <div>مرحبًا {data.name}!</div>; } // مع الخيارات function UserList() { const { data, error, isLoading, mutate } = useSWR( '/api/users', fetcher, { refreshInterval: 3000, // إعادة الجلب كل 3 ثوانٍ revalidateOnFocus: true, // إعادة التحقق عند التركيز على النافذة revalidateOnReconnect: true, // إعادة التحقق عند إعادة الاتصال dedupingInterval: 2000, // إلغاء تكرار الطلبات خلال ثانيتين onSuccess: (data) => { console.log('تم تحميل البيانات:', data); } } ); const handleAddUser = async (newUser) => { // تحديث تفاؤلي mutate([...data, newUser], false); // إجراء استدعاء API await fetch('/api/users', { method: 'POST', body: JSON.stringify(newUser) }); // إعادة التحقق mutate(); }; return ( <div> {data?.map(user => ( <div key={user.id}>{user.name}</div> ))} <button onClick={() => handleAddUser({ name: 'مستخدم جديد' })}> إضافة مستخدم </button> </div> ); } // الجلب المعتمد function UserPosts({ userId }) { const { data: user } = useSWR( userId ? `/api/users/${userId}` : null, fetcher ); const { data: posts } = useSWR( user ? `/api/posts?userId=${user.id}` : null, fetcher ); return ( <div> {posts?.map(post => ( <div key={post.id}>{post.title}</div> ))} </div> ); }
تخزين SWR المؤقت: يخزن SWR البيانات في الذاكرة ويشاركها عبر جميع المكونات التي تستخدم نفس المفتاح. هذا يعني أن مكونات متعددة تطلب نفس البيانات ستشارك طلبًا واحدًا. كن على دراية بإبطال الذاكرة المؤقتة عند تحديث البيانات.

React Query (TanStack Query)

مكتبة جلب البيانات الأقوى مع التخزين المؤقت المتقدم وإدارة الحالة:

// التثبيت: npm install @tanstack/react-query import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; // إعداد QueryClient في App import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60 * 1000, // 5 دقائق cacheTime: 10 * 60 * 1000, // 10 دقائق retry: 1, refetchOnWindowFocus: false } } }); function App() { return ( <QueryClientProvider client={queryClient}> {/* تطبيقك */} </QueryClientProvider> ); } // استعلام أساسي function Posts() { const { data, isLoading, isError, error } = useQuery({ queryKey: ['posts'], queryFn: async () => { const response = await fetch('/api/posts'); if (!response.ok) throw new Error('فشل الجلب'); return response.json(); } }); if (isLoading) return <div>جاري التحميل...</div>; if (isError) return <div>خطأ: {error.message}</div>; return ( <ul> {data.map(post => ( <li key={post.id}>{post.title}</li> ))} </ul> ); } // الطفرات function CreatePost() { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: async (newPost) => { const response = await fetch('/api/posts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newPost) }); return response.json(); }, onMutate: async (newPost) => { // إلغاء إعادة الجلب الصادرة await queryClient.cancelQueries({ queryKey: ['posts'] }); // لقطة للقيمة السابقة const previousPosts = queryClient.getQueryData(['posts']); // تحديث تفاؤلي queryClient.setQueryData(['posts'], (old) => [...old, newPost]); return { previousPosts }; }, onError: (err, newPost, context) => { // التراجع عند الخطأ queryClient.setQueryData(['posts'], context.previousPosts); }, onSettled: () => { // إعادة الجلب بعد الخطأ أو النجاح queryClient.invalidateQueries({ queryKey: ['posts'] }); } }); const handleSubmit = (e) => { e.preventDefault(); mutation.mutate({ title: e.target.title.value, content: e.target.content.value }); }; return ( <form onSubmit={handleSubmit}> <input name="title" placeholder="العنوان" /> <textarea name="content" placeholder="المحتوى" /> <button type="submit" disabled={mutation.isLoading}> {mutation.isLoading ? 'جاري الإنشاء...' : 'إنشاء منشور'} </button> {mutation.isError && ( <div>خطأ: {mutation.error.message}</div> )} </form> ); } // الترقيم function PaginatedPosts() { const [page, setPage] = useState(1); const { data, isLoading, isPreviousData } = useQuery({ queryKey: ['posts', page], queryFn: async () => { const response = await fetch(`/api/posts?page=${page}`); return response.json(); }, keepPreviousData: true // الاحتفاظ بالبيانات القديمة أثناء جلب الجديدة }); return ( <div> {data?.posts.map(post => ( <div key={post.id}>{post.title}</div> ))} <button onClick={() => setPage(old => Math.max(old - 1, 1))} disabled={page === 1} > السابق </button> <button onClick={() => setPage(old => old + 1)} disabled={isPreviousData || !data?.hasMore} > التالي </button> </div> ); }

اختيار الأداة المناسبة

متى تستخدم كل منها:
  • Fetch API: طلبات بسيطة لمرة واحدة، تطبيقات صغيرة، التعلم
  • Axios: تحتاج interceptors، معالجة أخطاء أفضل، إلغاء الطلبات
  • SWR: تطبيقات في الوقت الفعلي، تحتاج إعادة تحقق تلقائية، واجهة برمجة تطبيقات أبسط
  • React Query: تطبيقات معقدة، تحتاج تخزين متقدم، طفرات، ترقيم، تمرير لا نهائي

نمط متقدم: الجلب المسبق

حسّن تجربة المستخدم بجلب البيانات مسبقًا قبل أن يحتاجها المستخدمون:

// الجلب المسبق لـ React Query import { useQueryClient } from '@tanstack/react-query'; function PostList() { const queryClient = useQueryClient(); const handleMouseEnter = (postId) => { // جلب تفاصيل المنشور مسبقًا عند التمرير queryClient.prefetchQuery({ queryKey: ['post', postId], queryFn: () => fetch(`/api/posts/${postId}`).then(r => r.json()) }); }; return ( <div> {posts.map(post => ( <Link key={post.id} to={`/posts/${post.id}`} onMouseEnter={() => handleMouseEnter(post.id)} > {post.title} </Link> ))} </div> ); }

تمرين عملي 1: بحث المنتجات مع Debouncing

ابنِ قائمة منتجات قابلة للبحث:

  1. أنشئ حقل إدخال بحث مع استعلامات متباعدة (300ms)
  2. استخدم React Query لجلب نتائج البحث
  3. اعرض حالة التحميل أثناء البحث
  4. اعرض "لا توجد نتائج" عند إرجاع البحث فارغًا
  5. نفذ تخزين مؤقت لنتائج البحث

تمرين عملي 2: موجز التمرير اللانهائي

أنشئ موجز منشورات بتمرير لا نهائي:

  1. استخدم useInfiniteQuery hook من React Query
  2. حمّل 10 منشورات لكل صفحة
  3. نفذ اكتشاف التمرير مع IntersectionObserver
  4. اعرض دوار تحميل في الأسفل أثناء الجلب
  5. أضف زر "تحميل المزيد" كبديل

تمرين عملي 3: تحديثات المهام التفاؤلية

ابنِ تطبيق مهام مع تحديثات واجهة المستخدم الفورية:

  1. استخدم SWR أو React Query لقائمة المهام
  2. نفذ تحديثات تفاؤلية للإضافة/التبديل/الحذف
  3. اعرض رسالة تراجع إذا فشل طلب الخادم
  4. أضف آلية إعادة محاولة للطلبات الفاشلة
  5. اعرض مؤشر حالة المزامنة لكل مهمة

الخلاصة

اختر نهج جلب البيانات بناءً على تعقيد التطبيق. Fetch API يعمل للحالات البسيطة، Axios يضيف ميزات، بينما SWR و React Query توفر تخزين مؤقت قوي وإدارة حالة. التطبيقات الحديثة تستفيد من ميزات React Query المتقدمة مثل التحديثات التفاؤلية، والجلب المسبق، وإبطال الذاكرة المؤقتة التلقائي.