React: طلبات HTTP واسترجاع البيانات

آخر تحديث: 2026-08-26

عندما استدعى توم واجهة برمجة التطبيقات (API) لاسترداد قائمة بالمستخدمين في صفحة إدارة المستخدمين، ظهرت ثلاث مشكلات: أولاً، أدى التبديل السريع بين الصفحات إلى أن تستبدل استجابة الطلب السابق بيانات الطلب التالي (حالة تنافس)؛ ثانياً، عند حدوث خطأ في الشبكة، كانت الصفحة تصبح فارغة ببساطة دون عرض أي رسالة خطأ؛ وأخيراً، كان يتعين على كل صفحة تنفيذ نفس منطق الحالات الثلاث — التحميل، والخطأ، والبيانات — بشكل متكرر. وأدرك أن هناك حاجة إلى حل موحد لطلبات HTTP لإدارة دورة حياة الطلب بأكملها.


1. ما ستتعلمه



2. المخططات المفاهيمية

100%
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 تتمثل في إدارة ثلاثة متغيرات للحالة:

JSX
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

JSX
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">&#x26A0;</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

JSX
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>
  )
}
▶ جرّب الكود

مزايا الخطافات:

  1. تم تبسيط كود المكون بشكل كبير للتركيز على منطق العرض.
  2. صيانة موحدة للمنطق ثلاثي الحالات؛ ولا يلزم إجراء التغييرات إلا في مكان واحد
  3. يتم توفير الدالة 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 حل كل هذه المشكلات دفعة واحدة.

BASH
npm install axios

▶ المثال 3: تغليف مثيل axios

JSX
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 — إلغاء طلب

JSX
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>
  )
}

الآليات الرئيسية:

  1. في كل مرة يتغير فيها query، تستدعي دالة التنظيف useEffect الدالة controller.abort() لإلغاء الطلب السابق.
  2. تدخل الطلبات الملغاة إلى فرع «catch» ويتم تصفية هذه الطلبات بواسطة err.name !== 'AbortError' — وهذا يمنع ظهور واجهة المستخدم الخاصة بالخطأ عن طريق الخطأ.
  3. وفي النهاية، لن يؤدي سوى الاستجابة للطلب الأخير إلى تشغيل setResults، مما يقضي تمامًا على حالة التنافس.

▶ المثال 5: إلغاء طلب Axios

JSX
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 على ميزة إعادة المحاولة المدمجة، ولكن يمكن تنفيذها بسهولة باستخدام مُعترض:

JSX
// 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)
▶ جرّب الكود

التأجيل الأسي هو إحدى استراتيجيات إعادة المحاولة القياسية: حيث يزداد وقت الانتظار مع كل محاولة جديدة لتجنب الاستمرار في الضغط على الخادم عندما يكون مثقلًا بالأعباء بالفعل.



❓ أسئلة شائعة

س أيهما يجب أن أختار، fetch أم axios؟
ج fetch هي واجهة برمجة تطبيقات (API) مدمجة في المتصفح لا تتطلب أي تبعيات، وهي مناسبة للطلبات البسيطة. ومع ذلك، فإنه يتطلب معالجة يدوية لرموز حالة أخطاء HTTP (أخطاء 4xx/5xx لا تؤدي إلى تشغيل كتلة catch) ولا يدعم مراقبة تقدم الطلب. أما axios فيقوم تلقائيًا بتحليل JSON، ويدعم معترضات الطلبات/الاستجابات، ويجعل إلغاء الطلب أسهل، ويدعم متابعة تقدم التحميل. التوصية: استخدم fetch للمشاريع الصغيرة وaxios للمشاريع الكبيرة.
س لماذا تستخدم إدارة الحالات الثلاث ثلاثة ربطات useState منفصلة بدلاً من كائن واحد؟
ج تتيح الربطات الثلاثة المنفصلة useState للمكونات الاشتراك بدقة في التغييرات التي تطرأ على جزء محدد من الحالة. إذا استخدمت كائنًا واحدًا { data, loading, error }، فإن أي تغيير في أي حقل سيؤدي إلى إعادة عرض المكونات المشتركة في ذلك الكائن. ومع ذلك، في التطوير الفعلي، لا يوجد فرق كبير بين النهجين، لذا اختر أيهما يناسبك أكثر.
س هل يواصل الخادم معالجة الطلب بعد أن يقوم AbortController بإيقافه؟
ج نعم. يقوم AbortController فقط بمنع الواجهة الأمامية من انتظار الرد؛ أما الخادم فيستمر في استلام الطلب ومعالجته (لا يمكنه منع وصول الطلب إلى الخادم). بالنسبة لعمليات الكتابة (POST/PUT/DELETE)، يجب أن تكون الخلفية مصممة بحيث تكون متجانسة، أو يجب أن تنفذ الواجهة الأمامية إجراءات لمنع الإرسال المكرر.
س إذا تم استدعاء الخطاف المخصص useFetch في عدة مكونات، فهل ستتشارك هذه المكونات الحالة؟
ج لا. في كل مرة يستدعي فيها أحد المكونات useFetch()، فإنه ينشئ نطاقًا مستقلًا (إغلاقًا)، وبالتالي فإن قيم data وloading وerror المعنية لا تتداخل مع بعضها البعض. إذا كنت بحاجة إلى مشاركة بيانات الطلب عبر المكونات (مثلما يحدث عندما يعرض مكونان قائمة مستخدمين)، فستحتاج إلى إدارة الحالة العامة + TanStack Query (انظر الدرس التالي).
س ما الذي يجب أن آخذه في الاعتبار عند معالجة عمليات إعادة التوجيه 401 إلى صفحة تسجيل الدخول بشكل موحد في مُعترض الاستجابة؟
ج احرص على تجنب حدوث حلقة إعادة توجيه لا نهائية — إذا قامت صفحة تسجيل الدخول نفسها بإرسال طلب API (للتحقق من صلاحية الرمز المميز) وأعاد هذا الطلب أيضًا رمز خطأ 401، فسيؤدي ذلك إلى حلقة لا نهائية: «طلب → 401 → إعادة توجيه → طلب → 401». الحل: تحقق من مسار الصفحة الحالية في المعترض؛ إذا كان المستخدم موجودًا بالفعل في /login، فلا تقم بإعادة توجيهه.

📖 ملخص


📝 تمارين

  1. إنشاء مكون قائمة المستخدمين: استخدم طلب استرجاع (fetch) لجلب البيانات من https://jsonplaceholder.typicode.com/users وقم بتنفيذ أربع حالات لواجهة المستخدم: التحميل (رمز الدوران)، والخطأ (رسالة خطأ + زر إعادة المحاولة)، وعدم وجود بيانات (لا توجد بيانات متاحة)، والعرض العادي.
  2. استنادًا إلى المهمة المذكورة أعلاه، قم باستخراج المنطق ثلاثي الحالات إلى ربط مخصص useFetch، ثم استخدمه في مكونين مختلفين للتحقق مما إذا كانت الحالات مستقلة عن بعضها.
  3. إنشاء مكون بحث: اجعل حقل الإدخال يقوم بالبحث بناءً على ما يدخله المستخدم (بمحاكاة تأخير في واجهة برمجة التطبيقات (API) مدته 500 مللي ثانية)، واستخدم AbortController لإلغاء الطلب السابق غير المكتمل، وتأكد من أن البيانات لا تتشوه عند الكتابة بسرعة.
Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%