يكتب ٨٠٪ من مطوري React كوداً غير قابل للصيانة أو الأداء، حتى لو بدا يعمل. نكشف الفخاخ الخفية في الـ Hooks، الـ State Management، والـ Side Effects، ونقدم حلولاً هندسية تجعل الكود أسرع وأسهل في الصيانة.
في أحد المشاريع الكبيرة التي عملت عليها، كان لدينا مكون واحد في React يستهلك ٤٠٠ ميلي ثانية من وقت المعالجة في كل مرة يتفاعل المستخدم معه. المشكلة؟ لم يكن هناك أي تحذير في الـ console، ولم يظهر أي خطأ في الـ Performance Tab في Chrome DevTools. ببساطة، كان الكود "يعمل" — لكنه كان يعمل بطريقة خاطئة تماماً. بعد مراجعة الكود، اكتشفنا أن المطورين كانوا يستخدمون useEffect بطريقة تجعل الـ Event Loop يتوقف عن معالجة الأحداث الأخرى لمدة طويلة، مما يسبب تجمد واجهة المستخدم في بعض الأحيان. هذا ليس خطأ في React نفسه، بل في الطريقة التي نكتب بها الكود داخلها.
الغريب أن معظم المطورين لا يدركون أنهم يكتبون React بطريقة خاطئة حتى يواجهوا مشاكل حقيقية في الإنتاج. ربما لأنهم تعلموا من مصادر سطحية تركز على كتابة كود "يعمل" دون التفكير في كيفية عمله خلف الكواليس. في هذا المقال، سنفكك ثلاثة أخطاء شائعة في كتابة React: سوء استخدام الـ Hooks، إدارة الحالة بطريقة غير فعالة، والتعامل الخاطئ مع الـ Side Effects. سنشرح لماذا تحدث هذه الأخطاء، وكيف تؤثر على الأداء والصيانة، ونقدم حلولاً عملية تجعل كودك أفضل بكثير.
useEffect هو أحد أقوى الأدوات في React، لكنه أيضاً أكثرها سوء استخدام. المشكلة الأساسية هي أن المطورين يستخدمونه كسلة مهملات لكل شيء يحتاج إلى تنفيذ بعد Render، دون التفكير في تبعات ذلك على الأداء أو قابلية الصيانة. مثلاً، كثيراً ما أرى كوداً مثل هذا:
useEffect(() => {
fetchData();
setupEventListeners();
initializeThirdPartyLibrary();
return () => {
cleanupEventListeners();
};
}, []); // Empty dependency arrayهذا الكود يبدو بريئاً، لكنه في الواقع قنبلة موقوتة. أولاً، الـ Empty Dependency Array يجعل هذا الـ Effect يعمل مرة واحدة فقط عند تحميل المكون، لكن ماذا لو تغيرت بعض القيم التي يعتمد عليها الكود داخل الـ Effect؟ مثلاً، إذا كان fetchData يعتمد على قيمة من الـ Props، فلن يتم تحديث البيانات أبداً. ثانياً، تجميع عدة مهام غير مرتبطة في useEffect واحد يجعل من الصعب تتبع الأخطاء أو تعديل الكود لاحقاً. ثالثاً، إذا كانت أي من هذه المهام تستغرق وقتاً طويلاً (مثل initializeThirdPartyLibrary)، فإنها ستعطل الـ Event Loop وتجمد واجهة المستخدم.
الحل؟ تقسيم الـ Effects إلى وحدات صغيرة ومتخصصة. كل useEffect يجب أن يكون مسؤولاً عن شيء واحد فقط، ويجب أن تكون قائمة الـ Dependencies دقيقة تماماً. مثلاً:
useEffect(() => {
fetchData(userId);
}, [userId]); // Only re-run when userId changes
useEffect(() => {
const handler = () => handleResize();
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []); // Only run once for event listeners
useEffect(() => {
if (shouldInitializeLibrary) {
const cleanup = initializeThirdPartyLibrary(config);
return cleanup;
}
}, [config, shouldInitializeLibrary]); // Only re-run when config changesبهذه الطريقة، يصبح الكود أكثر قابلية للتنبؤ، وأسهل في التصحيح، وأقل عرضة للأخطاء المتعلقة بالـ Dependencies. أيضاً، إذا احتجت إلى تحسين الأداء لاحقاً، يمكنك بسهولة تحديد أي Effects يمكن تأجيلها أو تنفيذها بشكل غير متزامن.
السبب الرئيسي هو أن React لا تفرض عليك كتابة قائمة الـ Dependencies بشكل صحيح. إذا نسيت إضافة قيمة معينة إلى القائمة، قد لا تلاحظ المشكلة فوراً، خاصة في التطبيقات الصغيرة. لكن في التطبيقات الكبيرة، هذا يتحول إلى كابوس. مثلاً، في أحد المشاريع، كان لدينا مكون يعرض بيانات مستخدم بناءً على userId من الـ Props. المطور كتب useEffect بدون userId في قائمة الـ Dependencies، مما تسبب في عدم تحديث البيانات عند تغيير المستخدم. المشكلة لم تظهر إلا بعد أسبوعين من الإنتاج، عندما بدأ المستخدمون يشكون من أن البيانات لا تتغير عند التنقل بين الصفحات.
الحل التقني هو استخدام eslint-plugin-react-hooks الذي يأتي مع Create React App. هذه الأداة تكتشف تلقائياً إذا نسيت إضافة قيمة إلى قائمة الـ Dependencies، وتظهر تحذيراً في الـ console. لكن الأهم من ذلك هو فهم لماذا قائمة الـ Dependencies موجودة في المقام الأول: React تستخدمها لتحديد متى يجب إعادة تشغيل الـ Effect. إذا كانت القائمة غير كاملة، فإن React ستفترض أن الـ Effect لا يعتمد على أي شيء، وبالتالي لن يعاد تشغيله عند تغيير القيم ذات الصلة.
إدارة الحالة في React هي فن وعلم في نفس الوقت. المشكلة أن الكثير من المطورين إما يفرطون في استخدام Context API، أو يعتمدون بشكل مفرط على مكتبات خارجية مثل Redux دون حاجة حقيقية. مثلاً، في أحد المشاريع، كان لدينا مكون بسيط يعرض قائمة من المهام، وكان المطورون يستخدمون Redux لإدارة الحالة بالكامل، بما في ذلك حالة الفلترة والبحث. النتيجة؟ مكون بطيء جداً، وصعب جداً في الصيانة، رغم أن كل ما كان يحتاجه هو useState بسيط.
الخطأ هنا ليس في Redux نفسه، بل في استخدامه في المكان الخطأ. Redux مصمم لإدارة الحالة العالمية التي تحتاج إلى الوصول إليها من عدة أماكن في التطبيق. إذا كنت تستخدمه لإدارة حالة محلية داخل مكون واحد، فأنت تضيف تعقيداً غير ضروري. أيضاً، الكثير من المطورين لا يدركون أن تحديث الحالة في Redux يؤدي إلى إعادة Render لجميع المكونات المتصلة به، حتى لو لم تتغير البيانات التي يستخدمونها. هذا يمكن أن يسبب مشاكل أداء كبيرة في التطبيقات الكبيرة.
القاعدة البسيطة هي: استخدم useState لإدارة الحالة المحلية داخل مكون واحد. إذا كنت بحاجة إلى مشاركة الحالة بين مكونين أو ثلاثة، استخدم Context API. إذا كانت الحالة تحتاج إلى الوصول إليها من عدة أماكن في التطبيق، أو تحتاج إلى منطق معقد للتحديثات، ففكر في استخدام Redux أو Zustand. مثلاً:
// Bad: Using Redux for local state
const mapStateToProps = (state) => ({
searchTerm: state.tasks.searchTerm,
});
// Good: Using useState for local state
function TaskList() {
const [searchTerm, setSearchTerm] = useState('');
// ...
}أيضاً، الكثير من المطورين لا يدركون أن تحديث الحالة في React ليس دائماً تحديثاً مباشراً. عندما تستدعي setState أو useState setter، React قد تجمع عدة تحديثات معاً قبل إعادة Render المكون. هذا يعني أنك إذا قمت بتحديث الحالة عدة مرات في تتابع سريع، قد لا ترى كل التحديثات في واجهة المستخدم. الحل؟ إذا كنت بحاجة إلى ضمان تحديث الحالة فوراً، يمكنك استخدام useReducer بدلاً من useState، أو استخدام flushSync من react-dom في الحالات النادرة التي تحتاج فيها إلى ذلك.
إدارة الحالة بشكل خاطئ يمكن أن تؤدي أيضاً إلى تسريبات ذاكرة. مثلاً، إذا كان لديك useEffect يقوم بإضافة مستمع أحداث، وينسى إزالته في الـ Cleanup Function، فإن المكون سيبقى في الذاكرة حتى بعد إلغاء تحميله. هذا يمكن أن يسبب مشاكل كبيرة في التطبيقات التي تحتوي على مكونات يتم تحميلها وإلغاء تحميلها بشكل متكرر، مثل التطبيقات التي تستخدم التوجيه الديناميكي.
الحل؟ دائماً تأكد من أن كل useEffect يحتوي على Cleanup Function إذا كان يقوم بإضافة مستمعين للأحداث أو الاشتراكات. أيضاً، إذا كنت تستخدم مكتبات خارجية لإدارة الحالة، تأكد من أنك تفهم كيف تعمل خلف الكواليس. مثلاً، بعض مكتبات إدارة الحالة تحتفظ بمراجع للمكونات حتى بعد إلغاء تحميلها، مما يسبب تسريبات ذاكرة إذا لم يتم التعامل معها بشكل صحيح.
الـ Side Effects هي أي شيء يتفاعل مع العالم الخارجي، مثل طلبات الشبكة، أو الوصول إلى الـ DOM، أو استخدام مكتبات خارجية. المشكلة أن الكثير من المطورين لا يفهمون كيف تعمل الـ Side Effects في سياق React، مما يؤدي إلى كود بطيء أو غير موثوق به. مثلاً، في أحد المشاريع، كان لدينا مكون يقوم بإرسال طلب شبكة في كل مرة يتفاعل المستخدم معه. المطور استخدم useEffect بدون قائمة الـ Dependencies، مما تسبب في إرسال طلبات شبكة غير ضرورية في كل مرة يتم فيها إعادة Render المكون.
الحل؟ يجب أن تكون الـ Side Effects دقيقة ومحددة. إذا كنت بحاجة إلى إرسال طلب شبكة عند تغيير قيمة معينة، يجب أن يكون هذا واضحاً في قائمة الـ Dependencies. أيضاً، يجب أن تفكر في استخدام مكتبات مثل React Query أو SWR لإدارة طلبات الشبكة، لأنها توفر ميزات مثل الـ Caching والـ Retry والـ Prefetching بشكل تلقائي.
// Bad: Sending requests on every render
useEffect(() => {
fetchData();
}); // No dependency array
// Good: Sending requests only when needed
useEffect(() => {
fetchData(userId);
}, [userId]); // Only when userId changes
// Better: Using React Query for data fetching
function UserProfile({ userId }) {
const { data, isLoading } = useQuery(['user', userId], () => fetchUser(userId));
// ...
}أيضاً، الكثير من المطورين لا يدركون أن بعض الـ Side Effects يمكن أن تكون I/O Bound، مما يعني أنها ستعطل الـ Event Loop إذا لم يتم التعامل معها بشكل صحيح. مثلاً، قراءة ملف من القرص أو إرسال طلب شبكة يمكن أن يستغرق وقتاً طويلاً، مما يسبب تجمد واجهة المستخدم. الحل؟ استخدام الـ Web Workers للعمليات الثقيلة، أو استخدام مكتبات مثل react-use-webworker التي توفر واجهة بسيطة لاستخدام الـ Web Workers في React.
الـ Blocking Calls هي أي عملية تستغرق وقتاً طويلاً وتوقف الـ Event Loop عن معالجة الأحداث الأخرى. في React، هذا يمكن أن يسبب تجمد واجهة المستخدم، خاصة إذا كانت العملية تحدث في الـ Main Thread. مثلاً، إذا كان لديك مكون يقوم بمعالجة بيانات كبيرة في useEffect، فإن هذا سيوقف الـ Event Loop حتى تنتهي المعالجة، مما يسبب تجمد واجهة المستخدم.
الحل؟ نقل العمليات الثقيلة إلى الـ Web Workers، أو استخدام الـ setTimeout لتقسيم العملية إلى أجزاء صغيرة يمكن للـ Event Loop معالجتها بين كل جزء وآخر. مثلاً:
function processLargeData(data) {
const chunkSize = 1000;
let index = 0;
function processChunk() {
const end = Math.min(index + chunkSize, data.length);
for (let i = index; i < end; i++) {
// Process data[i]
}
index = end;
if (index < data.length) {
setTimeout(processChunk, 0); // Allow event loop to process other events
}
}
processChunk();
}بهذه الطريقة، يمكنك تجنب تجميد واجهة المستخدم حتى أثناء معالجة البيانات الكبيرة. أيضاً، يمكنك استخدام مكتبات مثل p-limit أو bottleneck للتحكم في عدد العمليات المتزامنة، مما يمنع تحميل الـ CPU بشكل مفرط.
الكود الجيد في React ليس مجرد كود "يعمل" — بل هو كود قابل للصيانة، وفعال في الأداء، وسهل في التصحيح. لتحقق ذلك، تذكر هذه القواعد البسيطة:
في النهاية، كتابة React بالطريقة الصحيحة تتطلب فهماً عميقاً لكيفية عمل المكتبة خلف الكواليس، وليس مجرد حفظ بعض الأنماط الشائعة. كلما فهمت أكثر كيف تتعامل React مع الـ State والـ Effects والـ Rendering، كلما كتبت كوداً أفضل وأكثر كفاءة. وإذا كنت تريد نصيحة واحدة أخيرة: لا تثق أبداً في الكود الذي "يعمل" فقط — اسأل نفسك دائماً: هل هذا الكود قابل للصيانة؟ هل هو فعال في الأداء؟ وهل هو سهل في التصحيح؟ إذا كانت الإجابة على أي من هذه الأسئلة "لا"، فقد حان الوقت لإعادة التفكير في الطريقة التي تكتب بها React.