كيف تحول منطق متكرر في React إلى Custom Hooks ذكية؟ اكتشف أمثلة حقيقية من مشاريع الإنتاج، الأخطاء الشائعة، وكيفية تحسين الأداء باستخدام Hooks مخصصة تتعامل مع الـ Event Loop وMemory Leaks.
في أحد مشاريع الإنتاج الكبيرة لشركة SaaS عربية، كان لدينا مكون واحد يتكرر في 12 مكاناً مختلفاً: مكون لإدارة الـ Modal مع تحكم كامل في الـ Animation، الـ Focus Trapping، و الـ Keyboard Events. المشكلة؟ كل نسخة كانت تحتوي على 180 سطراً من الكود، و 3 حالات مختلفة للـ State، و 4 تأثيرات جانبية. وعندما أردنا تعديل سلوك الـ Escape Key، اضطررنا لتعديل 12 ملفاً. هنا ظهرت الحاجة لـ Custom Hook لا يعيد استخدام الكود فقط، بل يعزل المنطق المعقد في مكان واحد. السؤال الحقيقي: متى يجب أن تبني Custom Hook، ومتى يكون مجرد تعقيد غير ضروري؟
الـ Custom Hooks ليست مجرد طريقة لتنظيم الكود، بل هي أداة قوية لإعادة التفكير في كيفية بناء منطق التطبيق. في React، الـ Hooks الأصلية مثل useState و useEffect تقدم لك الأساس، لكن الـ Custom Hooks تسمح لك ببناء طبقات أعلى من التجريد. المشكلة أن الكثير من المطورين يستخدمونها بشكل سطحي، دون فهم كيف تؤثر على الـ Render Cycle أو الـ Memory Management. مثلاً، هل تعلم أن Custom Hook سيئة التصميم يمكن أن تسبب إعادة رسم غير ضرورية للمكونات، أو حتى Memory Leaks إذا لم يتم تنظيف الـ Effects بشكل صحيح؟
عندما تنشئ Custom Hook، فأنت في الواقع تنشئ دالة عادية، لكنها تتبع قواعد معينة. لكن خلف الكواليس، React يتعامل معها بشكل مختلف تماماً عن الدوال العادية. مثلاً، إذا كان لديك Custom Hook اسمه useFetch، وكل مرة تستدعيها داخل مكون، React يقوم بإنشاء نسخة مستقلة منها، مرتبطة بذلك المكون فقط. هذا يعني أن كل مكون يحصل على نسخة خاصة من الـ State والـ Effects داخل الـ Hook. لكن هنا تكمن المشكلة: إذا لم تكن حذراً، قد ينتهي بك الأمر مع 50 نسخة من نفس الـ Hook تعمل في نفس الوقت، وكل منها يستهلك ذاكرة ومعالج.
لنأخذ مثالاً عملياً: في مشروع حقيقي لشركة FinTech، كنا نستخدم Custom Hook لإدارة الـ WebSocket Connections. المشكلة ظهرت عندما لاحظنا أن التطبيق يستهلك ذاكرة أكثر مما يجب. بعد التحقيق، اكتشفنا أن كل مكون كان ينشئ اتصال WebSocket جديد، حتى لو كان الاتصال موجوداً بالفعل. الحل؟ استخدمنا الـ useRef لحفظ الـ Socket Connection بشكل مركزي، بحيث يتم إنشاء اتصال واحد فقط، ويستخدمه جميع المكونات. هذا النوع من التفاصيل هو ما يميز Custom Hook جيدة من سيئة.
// مثال على Custom Hook لإدارة WebSocket مع منع تكرار الاتصال
import { useState, useEffect, useRef } from 'react';
const useWebSocket = (url) => {
const [messages, setMessages] = useState([]);
const socketRef = useRef(null);
const isC useRef(false);
useEffect(() => {
if (!isConnectedRef.current) {
socketRef.current = new WebSocket(url);
isConnectedRef.current = true;
socketRef.current.onmessage = (event) => {
setMessages(prev => [...prev, event.data]);
};
socketRef.current.onclose = () => {
isConnectedRef.current = false;
};
}
return () => {
if (socketRef.current && isConnectedRef.current) {
socketRef.current.close();
isConnectedRef.current = false;
}
};
}, [url]);
const sendMessage = (message) => {
if (socketRef.current && isConnectedRef.current) {
socketRef.current.send(message);
}
};
return { messages, sendMessage };
};
// استخدام الـ Hook في المكون
const ChatComponent = () => {
const { messages, sendMessage } = useWebSocket('wss://example.com/chat');
// ... باقي الكود
};ليس كل منطق متكرر يستحق Custom Hook. القاعدة الأولى التي أتبعها في جميع مشاريعي: إذا كان المنطق يستخدم أكثر من مرتين، ويحتوي على حالة داخلية (State) أو تأثيرات جانبية (Effects)، فهو مرشح جيد. لكن هناك استثناءات: مثلاً، إذا كان المنطق بسيط جداً ولا يحتوي على أي State أو Effects، فقد يكون مجرد دالة مساعدة عادية أفضل. في أحد مشاريع الـ E-commerce، كان لدينا منطق لحساب الخصومات بناءً على الـ Cart Items. في البداية، استخدمنا Custom Hook، لكن بعد مراجعة الكود، وجدنا أنه مجرد دالة حسابية بحتة بدون أي State. لذا قمنا بتحويلها إلى دالة عادية واستخدمناها مباشرة داخل المكونات.
القاعدة الثانية: إذا كان المنطق معقداً ويحتاج إلى اختبار مستقل، فهو مرشح ممتاز لـ Custom Hook. مثلاً، في مشروع لـ Dashboard معقدة، كان لدينا منطق لإدارة الـ Drag and Drop مع الـ Keyboard Navigation. هذا المنطق كان يحتوي على 4 حالات مختلفة للـ State، و 3 تأثيرات جانبية، و 7 دوال مساعدة. بدلاً من كتابة كل هذا داخل المكون، قمنا بعزله في Custom Hook اسمه useDragDrop، وكتبنا له اختبارات مستقلة باستخدام Jest و React Testing Library. النتيجة؟ المكون أصبح أنظف، والاختبارات أصبحت أسهل بكثير.
في معظم مشاريع الـ Web Applications، ستجد نفسك تكتب نفس الكود لإدارة الـ Forms مع الـ Validation. في أحد مشاريع الـ SaaS، كان لدينا 15 نموذج مختلف، وكل نموذج يحتوي على نفس المنطق تقريباً: إدارة الـ State للـ Inputs، الـ Validation عند الـ Blur، وعرض الأخطاء. بدلاً من تكرار الكود، قمنا ببناء Custom Hook اسمه useForm يعالج كل هذا المنطق. لكن هنا تأتي التفاصيل المهمة: كيف تتعامل مع الـ Validation بشكل فعال دون إعادة رسم المكون في كل مرة يتغير فيها الـ Input؟
// Custom Hook لإدارة Forms مع الـ Validation الذكي
import { useState, useEffect } from 'react';
const useForm = (initialValues, validate) => {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [touched, setTouched] = useState({});
// Validate only when the field is touched
useEffect(() => {
if (isSubmitting) {
const validati validate(values);
setErrors(validationErrors);
if (Object.keys(validationErrors).length === 0) {
// Form is valid, proceed with submission
} else {
setIsSubmitting(false);
}
}
}, [values, isSubmitting, validate]);
const handleChange = (e) => {
const { name, value } = e.target;
setValues(prev => ({ ...prev, [name]: value }));
};
const handleBlur = (e) => {
const { name } = e.target;
setTouched(prev => ({ ...prev, [name]: true }));
const validationErrors = validate({ ...values, [name]: values[name] });
setErrors(prev => ({ ...prev, [name]: validationErrors[name] }));
};
const handleSubmit = (callback) => (e) => {
e.preventDefault();
setIsSubmitting(true);
const validationErrors = validate(values);
setErrors(validationErrors);
if (Object.keys(validationErrors).length === 0) {
callback(values);
} else {
setIsSubmitting(false);
}
};
return {
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
isSubmitting
};
};
// مثال على دالة الـ Validation
const validate = (values) => {
const errors = {};
if (!values.email) {
errors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) {
errors.email = 'Email is invalid';
}
if (!values.password) {
errors.password = 'Password is required';
} else if (values.password.length < 6) {
errors.password = 'Password must be at least 6 characters';
}
return errors;
};
// استخدام الـ Hook في المكون
const LoginForm = () => {
const {
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
isSubmitting
} = useForm({ email: '', password: '' }, validate);
const onSubmit = (values) => {
console.log('Form submitted:', values);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
type="email"
name="email"
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
/>
{touched.email && errors.email && <span>{errors.email}</span>}
<input
type="password"
name="password"
value={values.password}
onChange={handleChange}
onBlur={handleBlur}
/>
{touched.password && errors.password && <span>{errors.password}</span>}
<button type="submit" disabled={isSubmitting}>
Submit
</button>
</form>
);
};الخطأ الأول والأكثر شيوعاً هو تجاهل تنظيف الـ Effects داخل الـ Custom Hooks. في أحد مشاريع الـ Real-Time Chat، كنا نستخدم Custom Hook لإدارة الـ Notifications. المشكلة ظهرت عندما لاحظنا أن الـ Event Listeners لا يتم إزالتها عند إلغاء تحميل المكون، مما تسبب في Memory Leaks. الحل؟ دائماً استخدم الـ Cleanup Function داخل الـ useEffect. لكن هناك تفصيل مهم: إذا كان الـ Effect يعتمد على متغيرات خارجية، يجب إضافتها إلى قائمة الـ Dependencies، وإلا قد لا يتم تشغيل الـ Cleanup بشكل صحيح.
الخطأ الثاني هو استخدام الـ Custom Hooks لتجميع منطق لا علاقة له ببعضه. مثلاً، في أحد المشاريع، وجدنا Custom Hook اسمه useUserAndTheme يجمع بين منطق الـ User Authentication ومنطق الـ Theme Switching. هذا النوع من الـ Coupling يجعل الكود صعب الصيانة ويزيد من احتمالية الـ Bugs. القاعدة الذهبية: كل Custom Hook يجب أن يكون مسؤولاً عن شيء واحد فقط. إذا وجدت نفسك تضيف منطق غير مرتبط، فربما تحتاج إلى تقسيم الـ Hook إلى عدة Hooks أصغر.
// مثال على Custom Hook سيئة التصميم (تجميع منطق غير مرتبط)
const useUserAndTheme = () => {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
// منطق الـ User
useEffect(() => {
const fetchUser = async () => {
const resp await fetch('/api/user');
const data = await response.json();
setUser(data);
};
fetchUser();
}, []);
// منطق الـ Theme
useEffect(() => {
document.body.className = theme;
}, [theme]);
return { user, theme, setTheme };
};
// الحل: تقسيم الـ Hook إلى جزئين
const useUser = () => {
const [user, setUser] = useState(null);
useEffect(() => {
const fetchUser = async () => {
const response = await fetch('/api/user');
const data = await response.json();
setUser(data);
};
fetchUser();
}, []);
return user;
};
const useTheme = () => {
const [theme, setTheme] = useState('light');
useEffect(() => {
document.body.className = theme;
}, [theme]);
return [theme, setTheme];
};الـ Custom Hooks يمكن أن تكون سلاحاً ذا حدين عندما يتعلق الأمر بالأداء. من ناحية، يمكنها تحسين الأداء عن طريق تقليل تكرار الكود وإعادة الرسم. من ناحية أخرى، إذا لم تكن مصممة بعناية، يمكنها أن تسبب إعادة رسم غير ضرورية للمكونات. مثلاً، في أحد مشاريع الـ Dashboard، كان لدينا Custom Hook لإدارة الـ Filters في الجداول. المشكلة كانت أن كل تغيير في الـ Filter كان يسبب إعادة رسم لجميع مكونات الجدول، حتى تلك التي لا تعتمد على الـ Filter. الحل؟ استخدمنا الـ useMemo لحفظ نتائج الـ Filtering، بحيث لا يتم إعادة الحساب إلا عندما تتغير البيانات الأساسية أو الـ Filter نفسه.
هناك تقنية أخرى مهمة لتحسين الأداء وهي استخدام الـ useCallback للدوال التي تمرر إلى المكونات الأبناء. في أحد مشاريع الـ E-commerce، كان لدينا Custom Hook لإدارة الـ Cart. المشكلة كانت أن كل إضافة أو إزالة منتج كانت تسبب إعادة رسم جميع مكونات الـ Cart، حتى تلك التي لا تعتمد على الـ Cart Items. السبب؟ الدوال التي تمرر إلى المكونات الأبناء كانت تتغير في كل مرة يتم فيها تحديث الـ State. الحل؟ استخدمنا الـ useCallback لتثبيت هذه الدوال، بحيث لا تتغير إلا عندما تتغير الـ Dependencies المحددة.
// Custom Hook لإدارة الـ Cart مع تحسين الأداء
import { useState, useCallback, useMemo } from 'react';
const useCart = () => {
const [items, setItems] = useState([]);
// استخدام useCallback لتثبيت الدوال
const addItem = useCallback((item) => {
setItems(prev => [...prev, item]);
}, []);
const removeItem = useCallback((id) => {
setItems(prev => prev.filter(item => item.id !== id));
}, []);
// استخدام useMemo لحفظ النتائج المكلفة
const total = useMemo(() => {
return items.reduce((sum, item) => sum + item.price, 0);
}, [items]);
return { items, addItem, removeItem, total };
};
// استخدام الـ Hook في المكون
const CartComp () => {
const { items, addItem, removeItem, total } = useCart();
// المكونات الأبناء لن تعيد الرسم إلا عند تغير الـ items
return (
<div>
<CartItems items={items} onRemove={removeItem} />
<CartTotal total={total} />
<button onClick={() => addItem({ id: 1, price: 10 })}>
Add Item
</button>
</div>
);
};الـ Custom Hooks ليست مجرد أداة لتنظيم الكود، بل هي جزء من تحول أكبر في كيفية بناء تطبيقات الـ Frontend. في المستقبل، أتوقع أن نرى المزيد من الـ Custom Hooks التي تتعامل مع الـ Server State بدلاً من الـ Client State فقط. مثلاً، في مشروع حديث لشركة Tech، استخدمنا Custom Hook اسمه useServerState لإدارة الـ State الذي يأتي من الـ API. هذا الـ Hook يتعامل مع الـ Caching، الـ Revalidation، والـ Optimistic Updates بشكل تلقائي، مما يقلل من الحاجة إلى مكتبات خارجية مثل React Query أو SWR.
هناك اتجاه آخر مثير هو استخدام الـ Custom Hooks لبناء ما يسمى بـ "الـ Micro-Frontends". في أحد مشاريع الـ Enterprise، قمنا ببناء Custom Hook اسمه useMicroFrontend يسمح لمكونات مختلفة من تطبيقات مختلفة بالتواصل مع بعضها البعض عبر الـ Event Bus. هذا النهج يسمح لنا ببناء تطبيقات معقدة من وحدات صغيرة مستقلة، كل وحدة لها منطقها الخاص ومعالجتها الخاصة للـ State. النتيجة؟ تطبيقات أسهل في الصيانة وأكثر مرونة للتغييرات المستقبلية.
// مثال على Custom Hook لإدارة الـ Server State
import { useState, useEffect } from 'react';
const useServerState = (url, opti {}) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [lastFetched, setLastFetched] = useState(null);
const fetchData = async () => {
setLoading(true);
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
setLastFetched(new Date());
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [url]);
const revalidate = () => {
fetchData();
};
return { data, loading, error, lastFetched, revalidate };
};
// استخدام الـ Hook في المكون
const UserProfile = ({ userId }) => {
const { data: user, loading, error, revalidate } = useServerState(
`/api/users/${userId}`
);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
<button onClick={revalidate}>Refresh</button>
</div>
);
};إذا كنت تريد أن تبني Custom Hooks جيدة، تذكر هذه القاعدة الذهبية: كل Custom Hook يجب أن يكون له مسؤولية واحدة فقط، ويجب أن يكون مستقلاً تماماً عن سياق استخدامه. هذا يعني أنه يجب أن يعمل بنفس الكفاءة سواء استخدمته في مكون واحد أو في 50 مكوناً مختلفاً. أيضاً، دائماً اختبر الـ Hook بشكل مستقل قبل استخدامه في المكونات، لأن الـ Bug في الـ Hook يمكن أن يظهر في أماكن غير متوقعة. وأخيراً، لا تخف من تقسيم الـ Hook إلى عدة Hooks أصغر إذا أصبح معقداً جداً. الكود الجيد هو الكود الذي يمكنك فهمه بعد ستة أشهر دون الحاجة إلى قراءة التعليقات.