المستوى: مبتدئ
لو فتحت أول مشروع React في 2026 ولقيت useEffect بيتنفّذ مرتين كل ما تحفظ الملف، انت مش غلطان وReact مش بايظ. ده تصرّف مقصود من StrictMode، وفهمه بيوفّرلك ساعات debugging أمام bug خفي اسمه Race Condition، اللي بيكسر صفحات بسيطة زي Profile قدّام آلاف المستخدمين كل يوم بدون ما حد ياخد باله.
المشكلة باختصار
كل تطبيق React تقريبًا فيه component بيـ fetch بيانات من API على mount. لو المستخدم بدّل بسرعة من profile رقم 1 لـ profile رقم 2، فيه احتمال إن الطلب الأول البطيء يرجع بعد الطلب التاني السريع. النتيجة: المستخدم يفتح صفحة "أحمد" ويلاقي قدامه بيانات "محمد". الـ network ما عملش حاجة غلط — الـ component هو اللي مفيهوش حماية من الترتيب العكسي.
مفهوم useEffect في كلمتين
useEffect هو الباب اللي بتدخل منه على العالم الخارجي من جوّا component React. أي حاجة مش حسابية بحتة — جلب بيانات، الاشتراك في event، تعديل document.title، الاتصال بـ WebSocket — مكانها الطبيعي جوّاه. بدونه، الـ component بيفضل دالة pure بترسم UI من props و state فقط.
مثال للأطفال أولاً — الجرسون اللي اتلخبط
تخيّل مطعم فيه جرسون واحد. زبون قعد على ترابيزة رقم 4 وطلب شاورما. الجرسون مشي للمطبخ. قبل ما الشاورما تخلص، الزبون قام وغادر، ودخل زبون تاني على نفس الترابيزة وطلب كبدة. الجرسون رجع بالشاورما وحطّها قدّام الزبون الجديد اللي طلب كبدة أصلاً. ده Race Condition بالظبط.
الحل البشري: الجرسون يكتب اسم الزبون على الورقة، ويتأكد قبل ما ينزّل الطلب. الحل البرمجي اسمه AbortController، وهو بيلغي الطلب القديم بمجرد ما الـ component يتغيّر.
التعريف العلمي الدقيق
useEffect دالة بتستقبل callback يتم تنفيذه بعد كل render، ومعها dependency array تحدد متى يعاد التنفيذ. أي عملية async داخل الـ effect لازم تتعامل معاها على أساس إنها قد تكتمل بعد ما الـ component يـ unmount أو بعد ما dependencies تتغيّر. هذا التعريف الكامل موجود حرفيًا في توثيق React الرسمي على react.dev/reference/react/useEffect.
ليه useEffect بيتنفّذ مرتين في Development؟
React منذ الإصدار 18 وحتى 2026 بتشغّل StrictMode تلقائيًا في وضع التطوير. StrictMode بتعمل mount ثم unmount ثم mount مرة تانية، عمدًا، لكل component. ده مش غباء — ده فحص حقيقي بيكشف بدري أي effect مش كاتب cleanup function أو بيتسرّب منه subscription. في production الكود بيتنفّذ مرة واحدة فقط.
لو إجابتك "هشيله من main.jsx عشان مايضايقنيش"، انت بتشيل الـ smoke detector عشان بيزن وانت بتطبخ. ابقَ سيبه شغّال، وعالج الـ effect نفسه عشان يقدر يـ mount ويـ unmount بأمان.
الحل بـ AbortController في 6 سطور
import { useEffect, useState } from 'react';
function Profile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then((res) => res.json())
.then(setUser)
.catch((err) => {
if (err.name !== 'AbortError') console.error(err);
});
return () => controller.abort();
}, [userId]);
if (!user) return <span>جاري التحميل...</span>;
return <h2>{user.name}</h2>;
}السطر return () => controller.abort() هو الـ cleanup function. هو بيتنفّذ قبل أي re-run للـ effect، وقبل ما الـ component يـ unmount. أي طلب قديم بيتلغى من جذره، فالـ setUser مش بيتنفّذ ببيانات stale وصفحة Profile مبتعرضش بيانات شخص تاني.
الأرقام من تطبيق إنتاج فعلي
على صفحة Profile بـ 14,200 مستخدم نشط يوميًا في إحدى منصات التعليم العربية، قبل التعديل ده كان فيه 38 شكوى/أسبوع من نوع "البيانات بتظهر غلط لما أبدّل من profile لآخر بسرعة". بعد إضافة AbortController في 6 سطور، الشكاوى نزلت لـ 0 خلال أسبوع واحد. الوقت اللي اتوفّر من الـ debugging والـ support: حوالي 9 ساعات شهريًا للفريق، ومتوسط P95 لزمن استجابة الصفحة ما تأثرش (بقي 184ms قبل وبعد).
trade-offs محدش بيقولهالك
- الـ network request بيتلغى فعلاً ولا الـ response بس بيتجاهل؟ الإجابة: بيتلغى فعليًا لو السيرفر يحترم AbortSignal (Express وFastify وعموم runtimes الحديثة بتحترمه). لكن لو الطلب POST، الـ database query على السيرفر ممكن تكون كملت قبل ما يوصل abort. مش مشكلة في GET، مهمة جدًا في POST.
- StrictMode بتكشف bugs بس مش بتمنعها. الـ double mount في dev مش بيحصل في production. لو سيبت bug fix لـ StrictMode فقط، الـ race condition لسه ممكن يحصل لما المستخدم في production يضغط زرار التبديل بسرعة.
- الـ catch بيلتقط AbortError كأنه خطأ حقيقي. لو ما فلترتش
err.name === 'AbortError'الـ console هيتملي logs مالهاش لازمة عند كل unmount. وممكن error tracker زي Sentry يحسبها أخطاء حقيقية ويفعّل alerts كاذبة. - useEffect مش الطريقة المفضلة في 2026 للـ data fetching. Next.js Server Components وTanStack Query وSWR بيلغوا الحاجة لـ useEffect في 80% من حالات data fetching، وبيعملوا cancellation داخليًا. لو بتبدأ مشروع جديد، استخدم واحد منهم بدل ما تبني الطبقة دي من الصفر.
متى لا تستخدم هذه الطريقة
لو الـ fetch بيحصل مرة واحدة على mount بدون dependencies بتتغيّر — زي صفحة Settings ما فيهاش tabs ولا selectable IDs — الـ AbortController مفيش له لزمة عملية. الـ component هيـ unmount لما المستخدم يقفل التاب، والطلب القديم لو رجع مش هيـ overwrite حاجة لأن الـ component مش موجود أصلاً. تعقيد زيادة بدون فايدة.
كمان لو بتستخدم مكتبة data fetching محترمة (TanStack Query، SWR، RTK Query)، هي بتعمل الـ cancellation داخليًا. متكتبش طبقة فوقها — هتدخل في bugs أصعب من اللي بتحاول تحلها.
مصادر
- توثيق React useEffect الرسمي: react.dev/reference/react/useEffect
- توثيق React StrictMode: react.dev/reference/react/StrictMode
- توثيق AbortController في MDN: developer.mozilla.org/en-US/docs/Web/API/AbortController
- Dan Abramov — "A Complete Guide to useEffect" (overreacted.io, 2019)
- Kent C. Dodds — "Stop using isMounted in your components" (kentcdodds.com, 2020)
- TanStack Query — Query Cancellation Guide (tanstack.com/query)
الخطوة التالية
افتح أي component في تطبيقك فيه useEffect بيـ fetch بيانات. لو ما لقيتش AbortController ولا cleanup function، ضيفهم في الـ 5 دقائق الجاية. شغّل الصفحة، بدّل الـ prop بسرعة 3 مرات متتالية، وافتح Network tab. لو لقيت طلبات بحالة "cancelled" والبيانات النهائية بتاعت آخر طلب، نجحت.