مشكلات React.useEffect الشائعة وكيفية إصلاحها بطريقة صحيحة
مقدمة: لماذا يسبب React.useEffect كثيراً من الالتباس؟
أصبحت React Hooks جزءاً أساسياً من تطوير واجهات المستخدم الحديثة، ومعظم المطورين باتوا يستخدمونها براحة في الحالات اليومية المعتادة. ومع ذلك، لا يزال useEffect من أكثر الأجزاء التي تؤدي إلى أخطاء دقيقة ومزعجة، خاصة عند التعامل مع dependency array وتمرير الكائنات أو الدوال إلى custom hooks.
في هذا المقال، سنستعرض مشكلة شائعة جداً في React.useEffect، ونشرح سبب حدوثها، ثم ننتقل إلى حلول عملية ونظيفة تحافظ على سلوك التطبيق وتلتزم في الوقت نفسه بقواعد ESLint.

سيناريو عملي: جلب بيانات المستخدم داخل React component
لنفترض أننا نبني تطبيقاً باستخدام React، ونريد عرض اسم المستخدم الحالي داخل أحد المكونات. لكن قبل ذلك، نحتاج إلى جلب بياناته من API أو من ملف بيانات مثل users.json.
وبما أن بيانات المستخدم قد نحتاج إليها في أكثر من مكان داخل التطبيق، فمن الأفضل عزل منطق جلب البيانات في custom hook مخصص مثل useUser.
قد يبدو المكون بهذا الشكل:
const Component = () => {
// useUser custom hook
return <div>{user.name}</div>;
};
من حيث الشكل، يبدو الأمر بسيطاً للغاية. لكن التعقيد يبدأ عندما نكتب الـ hook نفسه.
بناء custom hook باسم useUser
في الخطوة التالية، ننشئ hook مسؤولاً عن استقبال المستخدم، ثم جلب البيانات المرتبطة به وتخزينها في الحالة.
const useUser = (user) => {
const [userData, setUserData] = useState();
useEffect(() => {
if (user) {
fetch("users.json").then((response) =>
response.json().then((users) => {
return setUserData(users.find((item) => item.id === user.id));
})
);
}
}, []);
return userData;
};
ما الذي يحدث هنا بالتحديد؟
- يتحقق الـ hook أولاً من وجود الكائن
user. - بعد ذلك يتم تنفيذ
fetchلقراءة البيانات منusers.json. - ثم تتم عملية البحث عن المستخدم المطابق عبر
id. - وأخيراً يتم حفظ النتيجة في
userDataباستخدامsetUserData.
في النهاية يعيد الـ hook قيمة userData ليستخدمها أي مكون يستدعيه.
ملاحظة: هذا المثال تعليمي ومبسّط لتوضيح الفكرة فقط، أما في التطبيقات الحقيقية فغالباً ما تكون إدارة جلب البيانات أكثر تعقيداً، خاصة عند التعامل مع التحميل، والأخطاء، والتخزين المؤقت، وإعادة المحاولة.
استخدام useUser داخل المكون
بعد إنشاء الـ hook، يمكننا توصيله داخل المكون بالشكل التالي:
const Component = () => {
const user = useUser({ id: 1 });
return <div>{user?.name}</div>;
};
ظاهرياً، كل شيء يعمل كما هو متوقع. لكن عند فحص المحرر أو الطرفية، ستظهر رسالة تحذير من ESLint.
تحذير ESLint exhaustive-deps: ما معناه؟
التحذير الشهير يكون عادة بالشكل التالي:
React Hook useEffect has a missing dependency: 'user'.
Either include it or remove the dependency array. (react-hooks/exhaustive-deps)
هذه الرسالة تعني أن useEffect يستخدم المتغير user داخله، لكنه غير مذكور في dependency array. ومن منظور React، هذا قد يؤدي إلى سلوك غير متوقع لأن التأثير يعتمد على قيمة خارجية غير مراقبة.
وهنا يقع كثير من المطورين في المأزق: إذا أضفنا user إلى المصفوفة، تبدأ مشكلة جديدة.
المشكلة الحقيقية: إعادة التصيير اللانهائية في React
قد نحاول إصلاح التحذير عبر تعديل الكود بهذه الطريقة:
const useUser = (user) => {
const [userData, setUserData] = useState();
useEffect(() => {
if (user) {
fetch("users.json").then((response) =>
response.json().then((users) => {
return setUserData(users.find((item) => item.id === user.id));
})
);
}
}, [user]);
return userData;
};
لكن النتيجة هنا غالباً ستكون إعادة تصيير متكررة بلا توقف. فما السبب؟
لماذا يحدث هذا السلوك؟
السبب ليس أن قيمة id تغيّرت، بل لأن React يقارن الكائنات بالمراجع لا بالمحتوى فقط. فعندما نكتب داخل المكون:
const user = useUser({ id: 1 });
فنحن في كل عملية render ننشئ كائناً جديداً في الذاكرة، حتى لو كان يحمل نفس القيمة { id: 1 }. وبما أن المرجع تغيّر، فإن useEffect يعتبر أن التبعية user تغيّرت، فيُعاد تنفيذ التأثير من جديد.
بعد تنفيذ fetch يتم تحديث الحالة عبر setUserData، وهذا يؤدي إلى re-render جديد، ثم يُنشأ كائن جديد مرة أخرى، وتستمر الحلقة بلا نهاية.
ملخص المشكلة في نقاط
- تمرير كائن جديد داخل المكون في كل
render. useEffectيعتمد على هذا الكائن داخلdependency array.- React يقارن المراجع المرجعية للكائنات وليس شكلها فقط.
- تحديث الحالة يؤدي إلى إعادة التصيير.
- إعادة التصيير تنشئ كائناً جديداً، فتتكرر الدورة.
هل تجاهل تحذير ESLint حل مناسب؟
الحل السهل ظاهرياً هو حذف التبعية من dependency array أو تجاهل تحذير ESLint. لكن هذا النهج غير مستحسن إطلاقاً في المشاريع الاحترافية.
تجاهل قاعدة react-hooks/exhaustive-deps قد يؤدي إلى:
- أخطاء منطقية يصعب تتبعها لاحقاً.
- نتائج قديمة بسبب استخدام قيم غير محدثة داخل التأثير.
- سلوك غير متسق عند تغير المدخلات أو انتقال المكون بين الحالات المختلفة.
لذلك، الأفضل دائماً هو فهم سبب المشكلة وإصلاح البنية البرمجية بدلاً من إخفاء التحذير.
الحل الأول: إخراج الكائن خارج المكون للحصول على مرجع ثابت
إذا كانت قيمة المستخدم ثابتة ولا تعتمد على props أو state، فالحل الأبسط هو تعريف الكائن خارج المكون حتى يحتفظ بنفس المرجع في كل مرة.
const userObject = { id: 1 };
const Component = () => {
const user = useUser(userObject);
return <div>{user?.name}</div>;
};
export default Component;
بهذا الشكل، لا يتم إنشاء كائن جديد عند كل render، وبالتالي تصبح التبعية مستقرة، ويتوقف useEffect عن العمل بشكل متكرر دون داعٍ.
الحل الثاني: استخدام useMemo عندما تعتمد القيمة على state أو props
أحياناً لا يكون من الممكن تعريف الكائن خارج المكون، لأن قيمته قد تعتمد على معطيات ديناميكية، مثل URL params أو props أو state.
في هذه الحالة، يمكننا الاستفادة من useMemo لإنشاء كائن محفوظ بمرجع ثابت ما دامت التبعيات لم تتغير.
const Component = () => {
const { userId } = useParams();
const userObject = useMemo(() => {
return { id: userId };
}, [userId]);
// Don't forget the dependencies here either!
const user = useUser(userObject);
return <div>{user?.name}</div>;
};
export default Component;
لماذا ينجح useMemo هنا؟
لأن useMemo لا يعيد إنشاء الكائن إلا عند تغيّر userId. وهذا يعني أن مرجع الكائن سيظل ثابتاً بين عمليات التصيير المتتالية طالما بقيت القيمة نفسها.
وهكذا يصبح useEffect داخل useUser أكثر استقراراً، ولا يعمل إلا عندما يتغير المستخدم فعلاً.
الحل الثالث: تمرير قيمة primitive بدلاً من كائن كامل
في كثير من الحالات، الحل الأفضل والأبسط هو ألا نمرر كائناً من الأصل، بل نمرر فقط القيمة التي يحتاجها الـ hook، مثل userId.
هذا الأسلوب يقلل التعقيد ويتجنب تماماً مشكلات referential equality.
const useUser = (userId) => {
const [userData, setUserData] = useState();
useEffect(() => {
fetch("users.json").then((response) =>
response.json().then((users) => {
return setUserData(users.find((item) => item.id === userId));
})
);
}, [userId]);
return userData;
};
const Component = () => {
const user = useUser(1);
return <div>{user?.name}</div>;
};
هذا الحل غالباً هو الأكثر نظافة، لأنه يجعل واجهة الـ hook أبسط ويعتمد على قيمة أولية primitive بدلاً من كائن يمكن أن يسبب تغيّرات مرجعية غير مرغوبة.
مقارنة بين الحلول المتاحة
| الحل | متى يستخدم؟ | المزايا | ملاحظات |
|---|---|---|---|
| تعريف الكائن خارج المكون | عند ثبات البيانات | سهل وواضح وسريع | غير مناسب للقيم الديناميكية |
| استخدام useMemo | عند اعتماد الكائن على props أو state | يحافظ على مرجع ثابت | يتطلب إدارة صحيحة للتبعيات |
| تمرير userId فقط | عندما لا تحتاج hook إلا إلى قيمة واحدة | الأبسط والأكثر أماناً | مفضل في أغلب السيناريوهات |
ماذا لو كانت التبعية دالة وليست كائناً؟
إذا كانت القيمة الممررة إلى الـ hook أو إلى useEffect عبارة عن دالة بدلاً من كائن، فالمشكلة الأساسية تبقى نفسها: يتم إنشاء مرجع جديد مع كل render.
في هذه الحالة، بدلاً من useMemo نستخدم عادة useCallback لتثبيت مرجع الدالة ومنع إعادة التنفيذ غير الضرورية.
- استخدم
useMemoلتثبيت القيم والكائنات. - استخدم
useCallbackلتثبيت الدوال.
أفضل ممارسات التعامل مع React.useEffect
- لا تتجاهل تحذيرات
ESLintإلا في حالات نادرة ومفهومة تماماً. - تجنب تمرير كائنات أو دوال جديدة مباشرة داخل المكونات إذا كانت ستستخدم كتبعيات.
- فضّل تمرير القيم الأولية
primitive valuesمتى أمكن. - استخدم
useMemoوuseCallbackعندما تحتاج إلى مرجع ثابت. - صمم custom hooks بواجهات بسيطة وواضحة لتقليل الأخطاء.
الخلاصة التقنية
المشكلة في React.useEffect لا تكمن عادة في الـ hook نفسه، بل في الطريقة التي نتعامل بها مع التبعيات، خصوصاً عند تمرير الكائنات والدوال. إذا فهمت أن React يعتمد على المقارنة المرجعية، ستصبح قادراً على تفسير كثير من حالات إعادة التصيير اللانهائية بسهولة. من الناحية العملية، يبقى تمرير القيم الأولية مثل userId هو الحل الأكثر أناقة، بينما يوفر useMemo وuseCallback بدائل فعالة عندما تكون البنية أكثر تعقيداً. بناء مكونات وhooks بواجهات بسيطة هو أحد أهم أسرار كتابة تطبيقات React مستقرة وقابلة للصيانة.