لجلب البيانات في React، ابدأ الطلب داخل useEffect، واحفظ الرد في الحالة، واعرض رسالة تحميل حتى يصل. ضع القيم التي يعتمد عليها الطلب، مثل معرّف، في مصفوفة الاعتماديات ليجلب التأثير من جديد عندما تتغير.
لا تستطيع الأمثلة في هذه الصفحة الوصول إلى الشبكة، لذا فإن fetchUser واجهة برمجية وهمية: وعد يُحل بعد مهلة، كما يفعل fetch. غيّر 800 إلى 3000 لترى نص التحميل يبقى مدة أطول.
مع واجهة برمجية حقيقية يبدو جسم التأثير هكذا:
useEffect(() => {
fetch('https://api.example.com/users/1')
.then((res) => res.json())
.then((data) => setUser(data));
}, []);
التحميل والخطأ والبيانات
للطلب ثلاث نتائج ينبغي أن تُظهرها الشاشة: ما زال يُحمَّل، أو فشل، أو انتهى. احتفظ بكل منها في الحالة، وأعد ضبطها عندما يبدأ التأثير طلبًا جديدًا.
انقر User 3: ترفض الواجهة الوهمية الطلب، وتحل رسالة الخطأ محل الاسم، وتُظهر وحدة التحكم الخطأ. تمسح الكتلة finally قيمة loading في المسارين، فلا يترك الطلب الفاشل مؤشر التحميل ظاهرًا أبدًا.
عندما يتحدث fetch مع خادم حقيقي، فإن رد 404 أو 500 لا يرفض الوعد. افحص res.ok وارمِ الخطأ بنفسك:
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
async/await داخل تأثير
لا يمكن للدالة التي تمررها إلى useEffect أن تكون async. تتوقع React أن تُرجع لا شيء أو دالة تنظيف، والدالة غير المتزامنة تُرجع دائمًا وعدًا.
// Wrong: the effect returns a promise
useEffect(async () => {
const data = await fetchUser(id);
setUser(data);
}, [id]);
// Right: define an async function inside and call it
useEffect(() => {
async function load() {
const data = await fetchUser(id);
setUser(data);
}
load();
}, [id]);
حالات السباق
عندما يتغير المعرّف بسرعة، يكون طلبان قيد التنفيذ في الوقت نفسه. ولا شيء يضمن أن يجيبا بالترتيب. إذا كان الأقدم أبطأ، فإنه يصل أخيرًا ويكتب فوق بيانات المعرّف الذي اختاره المستخدم مؤخرًا.
الإصلاح علامة ignore. لكل تشغيل للتأثير علامته الخاصة، وتضبطها دالة تنظيفه على true. ولأن React تشغّل التنظيف قبل التشغيل التالي، فإن الرد الآتي من تشغيل قديم يرى ignore === true ويُهمل. تعرض هذه الكتلة الملف الشخصي نفسه مرتين، دون العلامة ومعها:
انقر الزر وانتظر. يصل رد المستخدم 2 السريع أولًا، فيسجّل الملفان كلاهما ويحتفظان بـ Grace. وبعد نحو ثانية ونصف من النقرة يصل رد المستخدم 1 البطيء: يتحول الملف الذي بلا علامة إلى Ada رغم أن المستخدم 2 هو المحدد، بينما يسجّل الذي فيه العلامة dropped stale Ada ويحتفظ بـ Grace.
يظهر الخطأ نفسه في مربعات البحث والتبويبات وأي قائمة يرشحها الخادم. يغفل مثال التحميل أعلاه العلامة ليبقى قصيرًا، لكن كل جلب في تأثير ينبغي أن يحتوي عليها، أو على الإلغاء الموضح أدناه.
الإلغاء باستخدام AbortController
تُهمل علامة ignore الرد القديم، لكن الطلب نفسه يستمر حتى النهاية. ومع fetch الحقيقي يمكنك إلغاؤه. أنشئ AbortController في التأثير، ومرّر signal الخاصة به إلى fetch، واستدعِ abort() في دالة التنظيف:
useEffect(() => {
const controller = new AbortController();
async function load() {
try {
const res = await fetch(`/api/users/${id}`, { signal: controller.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
setUser(await res.json());
} catch (err) {
if (err.name === 'AbortError') return; // cancelled on purpose
setError(err);
}
}
load();
return () => controller.abort();
}, [id]);
يرفض fetch الملغى بخطأ AbortError، فتتخطاه كتلة catch بدلًا من عرضه كفشل. ويغطي الإلغاء أيضًا إزالة المكوّن: عندما يُزال المكوّن، تلغي دالة تنظيفه الطلب.
نقله إلى خطاف مخصص
التحميل والخطأ والحماية من السباق متماثلة في كل مكوّن يجلب البيانات. ضعها في خطاف مخصص فيطلب كل مكوّن البيانات في سطر واحد.
fetchPosts معرّفة خارج المكوّن، فلا تتغير هويتها أبدًا وهي آمنة في مصفوفة الاعتماديات. لو عُرّفت داخل App لكانت دالة جديدة في كل عرض، فيعمل التأثير بعد كل عرض، ولأن التأثير يضبط الحالة، فلن يتوقف عن الجلب أبدًا.
غيّر 500 إلى 2000 وانقر الزر مرتين متتاليتين: تُظهر وحدة التحكم طلبين، ولا تظهر إلا منشورات الموضوع الذي انتهيت إليه.
الجلب في معالج حدث
التأثير مخصص للبيانات التي يحتاجها المكوّن لأنه على الشاشة: صفحة ملف شخصي تحمّل ملفها. وعندما يحدث طلب لأن المستخدم فعل شيئًا، مثل الضغط على Search أو Save، فأجرِه في معالج الحدث. تعرف بالضبط ما أطلقه، ولا شيء يُعاد تشغيله عندما تتغير حالة لا علاقة لها.
async function handleSubmit(e) {
e.preventDefault();
setStatus('saving');
const res = await fetch('/api/notes', { method: 'POST', body: JSON.stringify({ text }) });
setStatus(res.ok ? 'saved' : 'error');
}
أخطاء شائعة
اعتماديات مفقودة. التأثير الذي يقرأ id لكن مصفوفته [] يجلب المستخدم الأول إلى الأبد. أدرج كل قيمة يستخدمها الطلب.
الجلب في جسم المكوّن. يعمل fetch خارج التأثير في كل عرض، وإذا ضبط الحالة فإنه يبدأ حلقة.
الاعتماد على أن fetch يرفض عند الأخطاء. لا يرفض إلا عندما تفشل الشبكة. افحص res.ok.
نسيان إعادة ضبط التحميل. عندما يتغير المعرّف، أعد loading إلى true، وإلا بقيت البيانات القديمة على الشاشة دون أي إشارة إلى أن بيانات جديدة في الطريق.
متى تستخدم مكتبة أو إطار عمل
الجلب في تأثير لا بأس به لبضعة طلبات. لكنه لا يخزّن مؤقتًا: افتح الملف الشخصي نفسه مرتين فيجلبه مرتين. ولا يشارك البيانات بين المكوّنات، ولا يعيد المحاولة عند الفشل، ولا يعيد الجلب عندما يستعيد التبويب التركيز. المكتبات تفعل هذا نيابة عنك:
import { useQuery } from '@tanstack/react-query';
function Profile({ id }) {
const { data, error, isPending } = useQuery({
queryKey: ['user', id],
queryFn: () => fetch(`/api/users/${id}`).then((res) => res.json()),
});
if (isPending) return <p>Loading...</p>;
if (error) return <p>{error.message}</p>;
return <p>{data.name}</p>;
}
TanStack Query وSWR هما الخياران الشائعان لبيانات جهة العميل. وتذهب أطر العمل أبعد فتحمّل البيانات على الخادم قبل أن تصل الصفحة إلى المتصفح: Next.js مع مكوّنات الخادم، وReact Router مع المحمّلات (loaders). وهذا يتجنب وميض التحميل وسلسلة الطلبات التي تبدأ عندما يجب أن تصل بيانات الأب قبل أن يتمكن الابن حتى من بدء الجلب.
React 19: الدالة use() مع Suspense
تضيف React 19 الدالة use، التي تقرأ وعدًا أثناء العرض. يُعلَّق المكوّن حتى يُحل الوعد، وتعرض أقرب حدود <Suspense> محتوى بديلًا في الأثناء، فلا تكون للمكوّن نفسه حالة تحميل:
import { use, Suspense } from 'react';
function Profile({ userPromise }) {
const user = use(userPromise);
return <p>{user.name}</p>;
}
<Suspense fallback={<p>Loading...</p>}>
<Profile userPromise={userPromise} />
</Suspense>
يجب أن يُنشأ الوعد خارج المكوّن (من إطار عمل، أو ذاكرة مؤقتة، أو أب)، لا داخل العرض الذي يقرؤه، وإلا بدأ كل عرض طلبًا جديدًا. تتناول صفحة الخطاف use ذلك بأمثلة قابلة للتشغيل.
الأسئلة الشائعة
كيف أجلب البيانات عندما يُحمَّل مكوّن React؟
ابدأ الطلب في useEffect مع القيم التي يعتمد عليها في مصفوفة الاعتماديات ([] إن لم توجد)، وخزّن النتيجة في الحالة بـ useState. واعرض رسالة تحميل حتى تصل البيانات.
لماذا لا يمكن أن تكون دالة useEffect غير متزامنة؟
الدالة غير المتزامنة تُرجع دائمًا وعدًا، وتتوقع React أن يُرجع التأثير لا شيء أو دالة تنظيف. اكتب دالة غير متزامنة داخل التأثير واستدعِها فورًا.
ما هي حالة السباق في جلب البيانات في React؟
عندما يتغير المدخل بسرعة، يكون طلبان قيد التنفيذ وقد يجيب الأقدم أخيرًا فيكتب فوق البيانات الأحدث. اضبط علامة ignore في دالة تنظيف التأثير وتخطَّ setState عندما تكون مضبوطة، أو ألغِ الطلب بـ AbortController.
هل أستخدم useEffect أم مكتبة لجلب البيانات؟
يعمل useEffect للتطبيقات الصغيرة ويستحق الفهم. أما للتخزين المؤقت وإزالة التكرار وإعادة المحاولة وإعادة الجلب، فمكتبة مثل TanStack Query أو SWR، أو تحميل البيانات المدمج في إطار عمل مثل Next.js، توفر عليك كتابة ذلك المنطق بنفسك.
كيف أعرض مؤشر تحميل أثناء تحميل البيانات؟
احتفظ بقيمة منطقية loading (أو نص status) في الحالة، واضبطها قبل بدء الطلب وامسحها عندما ينتهي، في مسار النجاح ومسار الخطأ كليهما. واعرض المؤشر ما دامت صحيحة.