React: طلبات HTTP واسترجاع البيانات
آخر تحديث: 2026-08-26
عندما استدعى توم واجهة برمجة التطبيقات (API) لاسترداد قائمة بالمستخدمين في صفحة إدارة المستخدمين، ظهرت ثلاث مشكلات: أولاً، أدى التبديل السريع بين الصفحات إلى أن تستبدل استجابة الطلب السابق بيانات الطلب التالي (حالة تنافس)؛ ثانياً، عند حدوث خطأ في الشبكة، كانت الصفحة تصبح فارغة ببساطة دون عرض أي رسالة خطأ؛ وأخيراً، كان يتعين على كل صفحة تنفيذ نفس منطق الحالات الثلاث — التحميل، والخطأ، والبيانات — بشكل متكرر. وأدرك أن هناك حاجة إلى حل موحد لطلبات HTTP لإدارة دورة حياة الطلب بأكملها.
1. ما ستتعلمه
- معايير الاختيار بين واجهة برمجة التطبيقات Fetch وـ Axios
- أفضل الممارسات لإدارة حالات «التحميل» و«الخطأ» و«البيانات»
- AbortController: يلغي الطلبات لمنع حدوث حالات التنافس
- تغليف مثيل Axios وتكوين المعترضات
- معالجة الأخطاء واستراتيجيات إعادة المحاولة التلقائية
2. المخططات المفاهيمية
flowchart LR
A[Submit a Request] --> B{loading = true}
B --> C[Request in progress]
C --> D{Success/Failure?}
D -->|Success| E[data = Response<br/>loading = false<br/>error = null]
D -->|Failure| F[error = Error Message<br/>loading = false<br/>data = null]
E --> G[Rendering Data]
F --> H{Can I try again??}
H -->|is | A
H -->|No| I[Display Error UI]
G --> J[Component Uninstallation?]
J -->|is | K[AbortController<br/>Cancel Request]
style B fill:#fff3e0,stroke:#f57c00
style D fill:#e1f5fe,stroke:#0288d1
style K fill:#ffcdd2,stroke:#d32f2f
دورة حياة الطلب: يبدأ التحميل → يتم تنفيذ الطلب → يتم تعيين البيانات في حالة النجاح / يتم تعيين الخطأ في حالة الفشل → يتم إلغاء الطلبات المعلقة عند إلغاء تثبيت المكون.
3. سيناريو واقعي
تتطلب صفحة إدارة المستخدمين في «توم» ما يلي: يجب أن يتم تحميل قائمة المستخدمين عند فتح الصفحة؛ ويجب عرض رمز الدوران أثناء تحميل القائمة؛ ويجب ظهور رسالة خطأ وزر «إعادة المحاولة» في حالة فشل التحميل؛ ويجب ألا تتعطل البيانات عند قيام المستخدمين بالتبديل بسرعة بين عرض القائمة وعرض التفاصيل؛ ويجب إعادة توجيه جميع طلبات واجهة برمجة التطبيقات (API) تلقائيًّا إلى صفحة تسجيل الدخول في حالة حدوث خطأ 401.
(1) وضع الدول الثلاث
الطريقة الأساسية لإجراء طلبات HTTP في React تتمثل في إدارة ثلاثة متغيرات للحالة:
const [data, setData] = useState(null) // Success Data
const [loading, setLoading] = useState(true) // Loading...
const [error, setError] = useState(null) // Error Message
لماذا يعد الفصل بين ثلاث حالات ضروريًا؟ لأن واجهة المستخدم تحتاج إلى عرض محتوى مختلف تمامًا في ثلاث حالات مختلفة:
| الحالة | البيانات | التحميل | الخطأ | سلوك واجهة المستخدم |
|---|---|---|---|---|
| قيد التحميل | null | true | null | عرض شريط التقدم أو شاشة الانتظار |
| النجاح | البيانات | false | null | عرض قائمة البيانات |
| فشل | null | false | رسالة خطأ | عرض رسالة الخطأ وزر «إعادة المحاولة» |
| بيانات فارغة | [] | false | null | عرض رسالة "لا توجد بيانات متاحة" |
▶ المثال 1: إدارة ثلاث حالات باستخدام fetch وuseEffect
import { useState, useEffect } from 'react'
function UserList() {
const [users, setUsers] = useState([]) // Data
const [loading, setLoading] = useState(true) // Loaded state
const [error, setError] = useState(null) // Error State
function fetchUsers() {
setLoading(true)
setError(null)
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}:${response.statusText}`)
}
return response.json()
})
.then(data => {
setUsers(data)
setLoading(false)
})
.catch(err => {
setError(err.message)
setLoading(false)
})
}
useEffect(() => {
fetchUsers()
}, [])
// --- Three-State Rendering ---
if (loading) {
return (
<div className="loading-state">
<div className="spinner" />
<p>Loading user data...</p>
</div>
)
}
if (error) {
return (
<div className="error-state">
<p className="error-icon">⚠</p>
<p>Failed to load:{error}</p>
<button onClick={fetchUsers}>Retry</button>
</div>
)
}
if (users.length === 0) {
return (
<div className="empty-state">
<p>No user data available</p>
</div>
)
}
return (
<ul>
{users.map(user => (
<li key={user.id}>
<strong>{user.name}</strong> — {user.email}
</li>
))}
</ul>
)
}
ملاحظة مهمة: لا يُطلق fetch() استثناءً إلا عند حدوث خطأ في الشبكة؛ ولن تؤدي رموز الحالة HTTP 4xx/5xx إلى تشغيل كتلة catch. ولذلك، يجب عليك التحقق يدويًّا من response.ok (أو response.status) داخل then وإطلاق خطأ بشكل استباقي في حالة الردود التي لا تندرج ضمن فئة 2xx.
(2) استخراج الخطافات المخصصة
من الواضح أنه من غير العملي كتابة منطق الحالات الثلاث مرارًا وتكرارًا في كل صفحة. قام توم باستخراج منطق الحالات الثلاث إلى «هوك» مخصص، يمكن استخدامه في أي مكون بسطر واحد فقط من التعليمات البرمجية.
▶ المثال 2: الخطاف المخصص useFetch
import { useState, useEffect } from 'react'
// General Data Request Hook
function useFetch(fetchFn, deps = []) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
function execute() {
setLoading(true)
setError(null)
fetchFn()
.then(result => {
setData(result)
setLoading(false)
})
.catch(err => {
setError(err.message)
setLoading(false)
})
}
useEffect(() => {
execute()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps)
return { data, loading, error, refetch: execute }
}
// ====== Usage ======
function UserList() {
const { data: users, loading, error, refetch } = useFetch(
() => fetch('https://jsonplaceholder.typicode.com/users')
.then(r => { if (!r.ok) throw new Error('Request Failed'); return r.json() }),
[]
)
if (loading) return <p>Loading......</p>
if (error) return <p>Error:{error} <button onClick={refetch}>Retry</button></p>
return (
<ul>
{users?.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
)
}
مزايا الخطافات:
- تم تبسيط كود المكون بشكل كبير للتركيز على منطق العرض.
- صيانة موحدة للمنطق ثلاثي الحالات؛ ولا يلزم إجراء التغييرات إلا في مكان واحد
- يتم توفير الدالة
refetchللمكون لتسهيل إجراء إعادة الطلب يدويًّا
4. أغلفة Axios المتقدمة
| الميزة | fetch | axios |
|---|---|---|
| التثبيت | مدمج في المتصفح | npm install axios |
| تحليل الاستجابة | يدوي res.json() |
التحويل التلقائي إلى JSON |
| اعتراض الطلبات/الاستجابات | غير مدمج | أداة اعتراض interceptors |
| إعدادات المهلة | يجب استخدامها مع خيارات AbortController |
timeout |
| معالجة الأخطاء | أخطاء HTTP 4xx/5xx لا تُطلق استثناءات | يتم إطلاق أخطاء HTTP تلقائيًا |
| Request Cancellation | AbortController |
CancelToken (Old) / AbortController (New) |
| TypeScript | يلزم التأكيد اليدوي على النوع | العناصر العامة axios.get<T>() |
مع توسع المشروع، وجد توم أنه مضطر إلى إضافة الرموز يدويًّا، والتعامل مع عمليات إعادة التوجيه 401، وتعيين فترات الانتظار لكل طلب — وهو ما كان مملًّا للغاية. يمكن لآليات إنشاء المثيلات والمعترضات في Axios حل كل هذه المشكلات دفعة واحدة.
npm install axios
▶ المثال 3: تغليف مثيل axios
import axios from 'axios'
// Get token(from localStorage or auth store)
function getToken() {
return localStorage.getItem('auth_token')
}
// Create axios Examples
const api = axios.create({
baseURL: '/api/v1', // Basic Path
timeout: 10000, // Timeout (10s)
headers: {
'Content-Type': 'application/json',
}
})
// ========== Request Interceptor ==========
api.interceptors.request.use(
config => {
// Auto-add Authorization header
const token = getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
// Request Logs(Development Environment)
if (process.env.NODE_ENV === 'development') {
console.log(`[API] ${config.method?.toUpperCase()} ${config.url}`, config.params || '')
}
return config
},
error => {
console.error('[API] Request configuration error:', error)
return Promise.reject(error)
}
)
// ========== Response Interceptors ==========
api.interceptors.response.use(
// Successful Response:Return directly data Field(Remove the outer packaging)
response => response.data,
// Failure Response:Unified Error Handling
error => {
if (error.response) {
// The server returned an error status code
const { status, data } = error.response
switch (status) {
case 401:
// Unauthorized → Clear token,Go to the Login Page
localStorage.removeItem('auth_token')
window.location.href = '/login'
break
case 403:
console.warn('[API] Access Denied')
break
case 404:
console.warn('[API] Resource does not exist')
break
case 500:
console.error('[API] Internal Server Error')
break
default:
console.error(`[API] HTTP ${status}:`, data?.message || 'Unknown error')
}
return Promise.reject(new Error(data?.message || `HTTP ${status}`))
}
if (error.code === 'ECONNABORTED') {
// Request timed out
return Promise.reject(new Error('Request timed out,Please check your internet connection.'))
}
// Network error (offline, DNS failure, etc.)
return Promise.reject(new Error('Network Connection Error'))
}
)
// ========== Export the packaged API Methods ==========
export const userApi = {
getList: (params) => api.get('/users', { params }),
getById: (id) => api.get(`/users/${id}`),
create: (data) => api.post('/users', data),
update: (id, data) => api.put(`/users/${id}`, data),
delete: (id) => api.delete(`/users/${id}`),
}
export const productApi = {
getList: (params) => api.get('/products', { params }),
getById: (id) => api.get(`/products/${id}`),
}
// ========== Used in components ==========
function UserTable() {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
userApi.getList({ page: 1, limit: 20 })
.then(data => {
setUsers(data)
setLoading(false)
})
.catch(err => {
setError(err.message)
setLoading(false)
})
}, [])
// ... Rendering Logic
}
قوة المعترضات: تقوم معترضات الطلبات بإدراج الرموز المميزة تلقائيًا، بينما تتولى معترضات الاستجابات معالجة عمليات إعادة التوجيه 401 تلقائيًا — ولا داعي للمكونات ومستخدمي واجهة برمجة التطبيقات (API) أن يقلقوا بشأن هذه المسائل الشاملة على الإطلاق.
5. إلغاء الطلب وشروط السباق
| السيناريوهات التنافسية | الأسباب | الحلول |
|---|---|---|
| حالة التنافس في إدخال البحث | الإدخال السريع يؤدي إلى إرسال طلبات متعددة؛ وتقوم الردود القديمة بالكتابة فوق الردود الجديدة | يقوم AbortController بإلغاء الطلبات القديمة |
| حالة التنافس عند تبديل الصفحة | استمرار وصول الردود على الطلبات القديمة بعد تبديل الصفحة | إلغاء عملية التنظيف في useEffect |
| النقرات المتكررة على الزر | يقوم المستخدم بالنقر على زر «إرسال» عدة مرات | تعطيل زر «إرسال» + «إلغاء» أثناء الطلب |
| حالة التنافس عند التبديل بين علامات التبويب | يؤدي التبديل السريع بين علامات التبويب إلى اختلال محاذاة البيانات | استخدم العلامة ignore لتجاهل الردود القديمة |
هذه هي المعضلة الأكثر خفية التي واجهها توم على الإطلاق. عندما يقوم المستخدم بالبحث بسرعة في حقل الإدخال — بكتابة «a» → «ab» → «abc» → «abcd» — إذا تباينت سرعات الشبكة، فقد يحدث ما يلي: تصل الاستجابة لـ «abcd» أولاً، تليها الاستجابة لـ «a» (لأن الطلب السابق لم يتم إلغاؤه). ونتيجة لذلك، تعرض الصفحة نتيجة "a" بدلاً من النتيجة الأحدث، وهي "abcd".
تُسمى هذه الظاهرة حالة التنافس. الحل: قبل بدء طلب جديد، قم بإلغاء الطلب السابق الذي لم يكتمل بعد.
▶ المثال 4: AbortController — إلغاء طلب
import { useState, useEffect } from 'react'
function SearchUsers() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [loading, setLoading] = useState(false)
useEffect(() => {
if (!query.trim()) {
setResults([])
return
}
// Create AbortController
const controller = new AbortController()
const signal = controller.signal
setLoading(true)
fetch(`/api/users/search?q=${encodeURIComponent(query)}`, { signal })
.then(res => res.json())
.then(data => {
setResults(data)
setLoading(false)
})
.catch(err => {
// Handle only non-canceled errors
if (err.name !== 'AbortError') {
console.error('Search Failed:', err)
setLoading(false)
}
})
// Cleanup Function:Component uninstallation or query Cancel the request when changes occur
return () => {
controller.abort()
}
}, [query])
return (
<div>
<input
placeholder="Search Users..."
value={query}
onChange={e => setQuery(e.target.value)}
/>
{loading && <p>Searching......</p>}
<ul>
{results.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
)
}
الآليات الرئيسية:
- في كل مرة يتغير فيها
query، تستدعي دالة التنظيف useEffect الدالةcontroller.abort()لإلغاء الطلب السابق. - تدخل الطلبات الملغاة إلى فرع «catch» ويتم تصفية هذه الطلبات بواسطة
err.name !== 'AbortError'— وهذا يمنع ظهور واجهة المستخدم الخاصة بالخطأ عن طريق الخطأ. - وفي النهاية، لن يؤدي سوى الاستجابة للطلب الأخير إلى تشغيل
setResults، مما يقضي تمامًا على حالة التنافس.
▶ المثال 5: إلغاء طلب Axios
import { useState, useEffect } from 'react'
import axios from 'axios'
function SearchProducts() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [loading, setLoading] = useState(false)
useEffect(() => {
if (!query.trim()) {
setResults([])
return
}
// axios Cancel Token
const source = axios.CancelToken.source()
setLoading(true)
axios.get('/api/products/search', {
params: { q: query },
cancelToken: source.token
})
.then(res => {
setResults(res.data)
setLoading(false)
})
.catch(err => {
if (!axios.isCancel(err)) {
console.error('Search Failed:', err)
setLoading(false)
}
// Cancelled requests are not processed
})
return () => {
source.cancel('The request has been canceled') // Reason for Cancellation
}
}, [query])
return (
<div>
<input
placeholder="Search for Products..."
value={query}
onChange={e => setQuery(e.target.value)}
/>
{loading && <p>Searching......</p>}
<ul>
{results.map(p => (
<li key={p.id}>{p.name} — ${p.price}</li>
))}
</ul>
</div>
)
}
▶ المثال 6: استراتيجية إعادة المحاولة التلقائية
نظرًا لأن طلبات الشبكة غير موثوقة، يريد توم إعادة محاولتها تلقائيًا عند فشلها (على سبيل المثال، إعادة المحاولة مرتين مع زيادة الفواصل الزمنية تدريجيًّا). لا تحتوي Axios على ميزة إعادة المحاولة المدمجة، ولكن يمكن تنفيذها بسهولة باستخدام مُعترض:
// Retry Interceptor
function setupRetryInterceptor(axiosInstance, maxRetries = 2) {
axiosInstance.interceptors.response.use(
response => response,
async error => {
const config = error.config
// Cases Where Retry Is Not Performed:Not configured、I've already tried again、It's not a network error
if (!config || config._retryCount >= maxRetries) {
return Promise.reject(error)
}
// Only in the event of a network error or 5xx Retry in Case of a Server Error
const status = error.response?.status
if (status && status < 500) {
return Promise.reject(error)
}
config._retryCount = (config._retryCount || 0) + 1
// Exponential backoff: 1st retry after 1s, 2nd retry after 2s
const delay = config._retryCount * 1000
await new Promise(r => setTimeout(r, delay))
console.log(`[API] Retry ${config._retryCount}/${maxRetries}: ${config.url}`)
return axiosInstance(config)
}
)
}
// When using
setupRetryInterceptor(api, 2)
التأجيل الأسي هو إحدى استراتيجيات إعادة المحاولة القياسية: حيث يزداد وقت الانتظار مع كل محاولة جديدة لتجنب الاستمرار في الضغط على الخادم عندما يكون مثقلًا بالأعباء بالفعل.
❓ أسئلة شائعة
catch) ولا يدعم مراقبة تقدم الطلب. أما axios فيقوم تلقائيًا بتحليل JSON، ويدعم معترضات الطلبات/الاستجابات، ويجعل إلغاء الطلب أسهل، ويدعم متابعة تقدم التحميل. التوصية: استخدم fetch للمشاريع الصغيرة وaxios للمشاريع الكبيرة.useState منفصلة بدلاً من كائن واحد؟useState للمكونات الاشتراك بدقة في التغييرات التي تطرأ على جزء محدد من الحالة. إذا استخدمت كائنًا واحدًا { data, loading, error }، فإن أي تغيير في أي حقل سيؤدي إلى إعادة عرض المكونات المشتركة في ذلك الكائن. ومع ذلك، في التطوير الفعلي، لا يوجد فرق كبير بين النهجين، لذا اختر أيهما يناسبك أكثر.useFetch في عدة مكونات، فهل ستتشارك هذه المكونات الحالة؟useFetch()، فإنه ينشئ نطاقًا مستقلًا (إغلاقًا)، وبالتالي فإن قيم data وloading وerror المعنية لا تتداخل مع بعضها البعض. إذا كنت بحاجة إلى مشاركة بيانات الطلب عبر المكونات (مثلما يحدث عندما يعرض مكونان قائمة مستخدمين)، فستحتاج إلى إدارة الحالة العامة + TanStack Query (انظر الدرس التالي)./login، فلا تقم بإعادة توجيهه.📖 ملخص
- تُعد إدارة الحالات الثلاث (التحميل / الخطأ / البيانات) حجر الزاوية في طلبات البيانات في React؛ حيث تغطي هذه الحالات الأربع لواجهة المستخدم جميع السيناريوهات.
- استخدام «الخطافات» المخصصة لاستخراج منطق الحالات الثلاث، مما يتيح تجنب تكرار الكود في كل مكون ويسمح بإجراء الصيانة بشكل مركزي
- مثيل Axios + المعترضات لتنفيذ الإدراج التلقائي للرموز المميزة، وعمليات إعادة التوجيه التلقائية لرمز الخطأ 401، والمعالجة الموحدة للأخطاء
- تقوم AbortController (fetch) / CancelToken (axios) بإلغاء الطلبات المعلقة، مما يؤدي إلى حل حالات التنافس بشكل كامل
- يلزم إلغاء الطلب في حالات التغيير المتكررة، مثل البحث، والتنقل بين الصفحات، والتبديل بين علامات التبويب.
📝 تمارين
- إنشاء مكون قائمة المستخدمين: استخدم طلب استرجاع (fetch) لجلب البيانات من
https://jsonplaceholder.typicode.com/usersوقم بتنفيذ أربع حالات لواجهة المستخدم: التحميل (رمز الدوران)، والخطأ (رسالة خطأ + زر إعادة المحاولة)، وعدم وجود بيانات (لا توجد بيانات متاحة)، والعرض العادي. - استنادًا إلى المهمة المذكورة أعلاه، قم باستخراج المنطق ثلاثي الحالات إلى ربط مخصص
useFetch، ثم استخدمه في مكونين مختلفين للتحقق مما إذا كانت الحالات مستقلة عن بعضها. - إنشاء مكون بحث: اجعل حقل الإدخال يقوم بالبحث بناءً على ما يدخله المستخدم (بمحاكاة تأخير في واجهة برمجة التطبيقات (API) مدته 500 مللي ثانية)، واستخدم AbortController لإلغاء الطلب السابق غير المكتمل، وتأكد من أن البيانات لا تتشوه عند الكتابة بسرعة.