كيف تحول منطق متكرر في React إلى Custom Hooks ذكية؟ اكتشف أمثلة حية من مشاريع حقيقية، تجنب الفخاخ الشائعة، واستفد من خبرة 10 سنوات في بناء تطبيقات قابلة للصيانة.
في أحد مشاريع الإنتاج الكبيرة لشركة تقنية ناشئة، كان لدينا مكون واحد فقط مسؤول عن إدارة حالة المستخدم، والتحقق من الصلاحيات، وجلب البيانات من ثلاثة مصادر مختلفة. المكون أصبح بطول ٨٠٠ سطر، وكل مرة نحتاج فيها نفس المنطق في مكان آخر، كنا ننسخه ولصقه مع تعديلات طفيفة. النتيجة؟ ١٢ نسخة مختلفة من نفس الكود، وكل منها يتصرف بطريقة مختلفة قليلاً. المشكلة الحقيقية لم تكن في طول الكود، بل في أن الـ Event Loop كان يتعامل مع نفس العمليات المتكررة في كل مكون، مما أدى إلى زيادة زمن الاستجابة من ١٢٠ مللي ثانية إلى ٤٥٠ مللي ثانية في أسوأ الحالات. هنا أدركنا أن الوقت قد حان لإعادة التفكير في كيفية تنظيم منطقنا في React.
Custom Hooks ليست مجرد ميزة جميلة في React، بل هي أداة قوية لإعادة هيكلة الكود بطريقة تجعل منطقك قابل لإعادة الاستخدام دون التضحية بالأداء أو الوضوح. لكن الكثير من المطورين يستخدمونها بطريقة سطحية، كأنهم يكتبون دوال مساعدة عادية. الحقيقة هي أن Custom Hooks تعمل على مستوى مختلف تماماً: فهي جزء من دورة حياة المكون، ولها وصول مباشر إلى حالة React الداخلية، ويمكنها حتى أن تسبب Memory Leaks إذا لم تُكتب بعناية. في هذا المقال، سنغوص عميقاً في كيفية بناء Custom Hooks قوية من أمثلة حقيقية في مشاريع إنتاجية، ونكشف عن التفاصيل التي لا يخبرك بها معظم الدروس السطحية.
الكثير من المطورين الذين ينتقلون إلى React من خلفيات أخرى يحاولون حل مشكلة تكرار الكود بكتابة دوال مساعدة عادية. مثلاً، بدلاً من كتابة نفس الكود لجلب البيانات في كل مكون، يكتبون دالة مثل fetchData() ويستخدمونها في كل مكان. لكن هذه الطريقة تفشل في عدة نقاط حاسمة. أولاً، الدوال العادية لا تستطيع الوصول إلى حالة React الداخلية أو الـ Hooks المدمجة مثل useState أو useEffect. ثانياً، إذا كنت بحاجة إلى إدارة حالة محلية داخل هذه الدالة، فستضطر إلى تمريرها كوسائط أو استخدام متغيرات خارجية، مما يجعل الكود معقداً ويصعب تتبع التغييرات. ثالثاً، الدوال العادية لا تشارك في دورة حياة المكون، مما يعني أنك ستضطر إلى إدارتها يدوياً، وهذا غالباً ما يؤدي إلى مشاكل في تزامن الحالة.
لنأخذ مثالاً عملياً من مشروع حقيقي: في منصة تعليمية إلكترونية، كان لدينا مكونات متعددة تحتاج إلى إدارة حالة البحث، بما في ذلك إدخال المستخدم، تصفية النتائج، وتخزين النتائج في ذاكرة التخزين المؤقت. باستخدام دوال مساعدة عادية، كان علينا تمرير حالة البحث كوسيط لكل دالة، وهذا أدى إلى كود متكرر ومعقد. وعندما قررنا استخدام Custom Hook اسمه useSearch، أصبح بإمكاننا إدارة كل هذه الحالة داخلياً، واستخدام useEffect لمراقبة تغييرات الإدخال، واستخدام useState لتخزين النتائج. النتيجة؟ الكود أصبح أقصر بنسبة ٦٠٪، والأداء تحسن بنسبة ٣٥٪ لأننا توقفنا عن إعادة تنفيذ عمليات البحث غير الضرورية.
// مثال سيء: دالة مساعدة عادية لا تستطيع إدارة الحالة الداخلية
function fetchData(url) {
let data = null;
let error = null;
let loading = true;
fetch(url)
.then(res => res.json())
.then(d => { data = d; loading = false; })
.catch(e => { error = e; loading = false; });
return { data, error, loading };
}
// المشكلة: الحالة لا تتزامن مع React، وستحتاج إلى إعادة استدعاء الدالة يدوياً
// مثال جيد: Custom Hook يدير الحالة داخلياً
function useFetch(url) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const c new AbortController();
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(d => { setData(d); setLoading(false); })
.catch(e => { if (e.name !== 'AbortError') setError(e); setLoading(false); });
return () => controller.abort(); // تنظيف عند إلغاء المكون
}, [url]);
return { data, error, loading };
}الكثير من الدروس تتوقف عند بناء Custom Hooks بسيطة مثل useFetch أو useLocalStorage، لكن في المشاريع الحقيقية، غالباً ما تحتاج إلى بناء hooks أكثر تعقيداً تتعامل مع حالات متداخلة، وتزامن العمليات، وإدارة الأخطاء بشكل ذكي. مثلاً، في مشروع لإدارة المهام، كنا بحاجة إلى hook اسمه useTaskManager يتعامل مع إنشاء المهام، تحديثها، حذفها، وتزامنها مع السيرفر. هذا Hook لم يكن مجرد واجهة لجلب البيانات، بل كان يدير حالة محلية مؤقتة للمهام التي لم تُرسل بعد إلى السيرفر، ويتعامل مع حالات الفشل في الاتصال، ويعيد محاولة الإرسال تلقائياً.
أحد التحديات الكبيرة في بناء Custom Hooks معقدة هو إدارة الاعتمادات بين التأثيرات الجانبية. مثلاً، إذا كان لديك useEffect يعتمد على قيمة معينة، وتحتاج إلى تشغيل تأثير آخر عند تغيير هذه القيمة، فمن السهل الوقوع في فخ الـ Infinite Loop إذا لم تكن حذراً. في مشروع آخر، كنا نبني hook اسمه useAnalytics يتتبع تفاعل المستخدم مع الصفحة ويرسل البيانات إلى خدمة تحليلية خارجية. المشكلة كانت في أن كل تغيير في حالة التفاعل كان يشغل useEffect مرة أخرى، مما يؤدي إلى إرسال بيانات مكررة. الحل؟ استخدمنا useRef لتخزين آخر حالة مرسلة، وقارناها مع الحالة الحالية قبل الإرسال.
function useAnalytics(eventName, metadata = {}) {
const lastSentRef = useRef(null);
const isMountedRef = useRef(false);
useEffect(() => {
if (!isMountedRef.current) {
isMountedRef.current = true;
return;
}
const currentState = { eventName, metadata };
if (JSON.stringify(lastSentRef.current) !== JSON.stringify(currentState)) {
lastSentRef.current = currentState;
// إرسال البيانات إلى خدمة التحليلات
analyticsService.track(eventName, metadata);
}
}, [eventName, metadata]);
}
// استخدامه في المكون
function UserProfile() {
const [userData, setUserData] = useState(null);
useAnalytics('profile_view', {
userId: userData?.id,
timestamp: Date.now()
});
// ... باقي الكود
}أحد أسوأ الكوابيس التي يمكن أن تواجهها مع Custom Hooks هو الـ Memory Leak. يحدث هذا عندما يكون لديك تأثير جانبي لا يُنظف بشكل صحيح عند إلغاء المكون، مما يؤدي إلى استمرار العمليات في الخلفية واستهلاك الموارد. مثلاً، إذا كان لديك Custom Hook يستخدم setInterval لتحديث البيانات بشكل دوري، ولم تقم بإزالة الـ Interval عند إلغاء المكون، فستجد أن الـ Interval يستمر في العمل حتى بعد اختفاء المكون من واجهة المستخدم. في أحد المشاريع، تسبب هذا في زيادة استهلاك الذاكرة بنسبة ٤٠٪ بعد بضع دقائق من استخدام التطبيق، مما أدى إلى تجمد المتصفح لدى بعض المستخدمين.
الحل؟ يجب أن يكون كل Custom Hook مسؤولاً عن تنظيف آثاره الجانبية. في المثال السابق، استخدمنا return function داخل useEffect لإزالة الـ Interval عند إلغاء المكون. لكن المشكلة تصبح أكثر تعقيداً عندما تتعامل مع مكتبات خارجية أو عمليات غير متزامنة معقدة. مثلاً، في مشروع يستخدم WebSocket للتواصل مع السيرفر، كان لدينا hook اسمه useWebSocketConnection. إذا لم نغلق الاتصال بشكل صحيح عند إلغاء المكون، فسيستمر الاتصال في العمل في الخلفية، مما يؤدي إلى إرسال واستقبال بيانات غير ضرورية. الحل كان استخدام AbortController لإلغاء العمليات غير المتزامنة، وإغلاق الاتصال عند إلغاء المكون.
function useWebSocketConnection(url) {
const [messages, setMessages] = useState([]);
const socketRef = useRef(null);
useEffect(() => {
const socket = new WebSocket(url);
socketRef.current = socket;
socket. (event) => {
setMessages(prev => [...prev, JSON.parse(event.data)]);
};
socket.onerror = (error) => {
console.error('WebSocket error:', error);
};
return () => {
if (socket.readyState === WebSocket.OPEN) {
socket.close(); // إغلاق الاتصال عند إلغاء المكون
}
};
}, [url]);
const sendMessage = (message) => {
if (socketRef.current?.readyState === WebSocket.OPEN) {
socketRef.current.send(JSON.stringify(message));
}
};
return { messages, sendMessage };
}خلال السنوات العشر الماضية، بنيت العشرات من Custom Hooks لمشاريع إنتاجية مختلفة، من منصات التجارة الإلكترونية إلى تطبيقات إدارة المشاريع. أحد الدروس الرئيسية التي تعلمتها هو أن أفضل Custom Hooks هي تلك التي تركز على حل مشكلة محددة بشكل جيد، بدلاً من محاولة أن تكون حلاً عاماً لكل شيء. مثلاً، في مشروع لإدارة المستودعات، كان لدينا hook اسمه useInventoryAlerts مسؤول فقط عن مراقبة مستويات المخزون وإرسال تنبيهات عندما تصل إلى حد معين. هذا Hook كان بسيطاً ومحدداً، مما جعله سهل الصيانة وسريع الأداء.
درس آخر مهم هو أن Custom Hooks يمكن أن تكون أداة قوية لتحسين أداء التطبيق إذا استخدمت بحكمة. في مشروع آخر، كنا نستخدم مكتبة خارجية لإدارة النماذج، وكانت هذه المكتبة تسبب إعادة تحميل المكونات بشكل متكرر عند تغيير الحقول. الحل؟ بنينا Custom Hook اسمه useFormOptimizer يستخدم useMemo وuseCallback لتقليل عدد إعادة التحميل. النتيجة؟ زمن الاستجابة لتحركات المستخدم تحسن من ٣٠٠ مللي ثانية إلى ٨٠ مللي ثانية، وهذا فرق ملحوظ في تجربة المستخدم.
على الرغم من قوة Custom Hooks، هناك حالات يكون فيها استخدامها غير مناسب أو حتى ضار. مثلاً، إذا كان لديك منطق بسيط جداً يُستخدم في مكون واحد فقط، فإن بناء Custom Hook قد يكون مبالغة. في إحدى المرات، رأيت مطوراً يبني Custom Hook لإدارة حالة زر واحد في مكون صغير. النتيجة؟ الكود أصبح أكثر تعقيداً من اللازم، وكان من الأسهل بكثير إدارة الحالة داخل المكون نفسه باستخدام useState.
حالة أخرى يجب تجنب Custom Hooks فيها هي عندما تحتاج إلى منطق يعتمد بشكل كبير على سياق محدد. مثلاً، إذا كان لديك منطق يعتمد على قيم من Context API أو Redux، فقد يكون من الأفضل إدارة هذا المنطق داخل المكون نفسه أو باستخدام Selectors في حالة Redux. في مشروع استخدمنا فيه Redux بشكل مكثف، حاولنا بناء Custom Hook لإدارة حالة معينة، لكننا وجدنا أن هذا يؤدي إلى تعقيد الكود ويجعل تتبع تدفق البيانات أكثر صعوبة. الحل الأفضل كان استخدام Selectors داخل المكونات للحصول على البيانات المطلوبة.
إذا وجدت نفسك في موقف تحتاج فيه إلى منطق متكرر ولكن Custom Hook يبدو مبالغاً فيه، فهناك بدائل ذكية يمكنك استخدامها. مثلاً، يمكنك بناء مكونات صغيرة قابلة لإعادة الاستخدام بدلاً من Custom Hooks. في مشروع لإدارة المهام، كان لدينا منطق متكرر لإظهار وإخفاء النوافذ المنبثقة. بدلاً من بناء Custom Hook، بنينا مكوناً صغيراً اسمه Modal يستخدم children pattern، مما جعل الكود أكثر قابلية لإعادة الاستخدام وأسهل في الصيانة.
// مكون قابل لإعادة الاستخدام بدلاً من Custom Hook
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
return (
<div className="modal-overlay">
<div className="modal-content">
{children}
<button {onClose}>Close</button>
</div>
</div>
);
}
// استخدامه في المكون
function TaskManager() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div>
<button onClick={() => setIsModalOpen(true)}>Open Modal</button>
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
<h2>Task Details</h2>
{/* محتوى النافذة */}
</Modal>
</div>
);
}إذا كنت تريد أن تبني Custom Hooks قوية وقابلة للصيانة، فاتبع هذه القاعدة الذهبية: اجعل كل Custom Hook مسؤولاً عن شيء واحد فقط، واجعله يفعل ذلك الشيء بشكل ممتاز. لا تحاول أن تجعل الـ Hook يحل كل المشاكل، بل اجعله يحل مشكلة محددة بشكل جيد. وعندما تكتب الـ Hook، تخيل أنك تكتب مكتبة صغيرة: يجب أن يكون سهل الاستخدام، وموثق جيداً، وخالي من الآثار الجانبية غير المتوقعة. وإذا وجدت نفسك تضيف الكثير من الخيارات والوسائط إلى الـ Hook، فهذا إشارة إلى أنه ربما يجب تقسيمه إلى hooks أصغر وأكثر تركيزاً.
وأخيراً، تذكر أن Custom Hooks ليست مجرد أداة لتنظيم الكود، بل هي طريقة لإعادة التفكير في كيفية بناء تطبيقات React. بدلاً من التفكير في المكونات كوحدات مستقلة، فكر في كيفية بناء منطق قابل لإعادة الاستخدام يمكن أن يعيش خارج المكونات نفسها. وعندما تفعل ذلك بشكل صحيح، ستجد أن تطبيقاتك تصبح أسرع، وأسهل في الصيانة، وأكثر متعة في العمل عليها.