اكتشف كيف تحول Custom Hooks في React من منطق متكرر إلى وحدات ذكية قابلة لإعادة الاستخدام، مع أمثلة حقيقية من مشاريع الإنتاج وكيفية تجنب الفخاخ الشائعة التي تكلف الفرق ساعات من Debugging.
في أحد مشاريع الإنتاج الكبيرة التي عملت عليها العام الماضي، كان لدينا مكون واحد فقط مسؤول عن إدارة الـ Form مع أكثر من ٢٠ حقل إدخال، وكل حقل يحتاج إلى منطق متكرر: التحقق من الصحة، إدارة حالة التحميل، التعامل مع الأخطاء، وتتبع التغيرات. النتيجة؟ مكون ضخم يتجاوز ٥٠٠ سطر من الكود، وكلما أردنا إضافة حقل جديد كنا ننسخ ولصق نفس الـ Logic مع تغيير بسيط في الأسماء. المشكلة لم تكن فقط في التكرار، بل في أن أي تعديل بسيط في منطق التحقق كان يتطلب تغيير ٢٠ مكان مختلف في الكود. هذا ليس مجرد كابوس للصيانة، بل هو دعوة مفتوحة للـ Bugs التي تظهر في أماكن غير متوقعة. هنا تأتي قوة Custom Hooks في React: إنها تسمح لك باستخراج هذا المنطق المتكرر إلى وحدة مستقلة يمكن إعادة استخدامها في أي مكان، دون الحاجة إلى تكرار الكود أو الجنون الذي يأتي معه.
لكن Custom Hooks ليست مجرد أداة لتنظيم الكود، بل هي نمط برمجي يسمح لك بفصل الـ Business Logic عن واجهة المستخدم بطريقة تجعل الكود أكثر قابلية للاختبار والصيانة. فكر فيها كدوال مستقلة تستفيد من ميزات React Hooks مثل useState وuseEffect، لكنها مصممة لحل مشكلة محددة في تطبيقك. الفرق بين استخدام Custom Hook واستخدام مكون عادي هو أن الـ Hook لا يرتبط بأي واجهة مستخدم، بل يركز فقط على المنطق، مما يجعله أكثر مرونة وقابلية لإعادة الاستخدام عبر مكونات مختلفة، وحتى عبر مشاريع مختلفة إذا تم تصميمه بشكل جيد.
التكرار في الكود ليس مجرد مسألة جمالية، بل هو مشكلة هندسية حقيقية تؤثر على أداء التطبيق وصيانته. عندما تكرر نفس المنطق في عدة مكونات، فإنك تخلق ما يسمى بـ "Single Source of Truth" مبعثر، حيث يصبح من الصعب تتبع مكان حدوث التغييرات أو الأخطاء. على سبيل المثال، إذا كان لديك منطق للتحقق من صحة البريد الإلكتروني في خمسة مكونات مختلفة، فإن تغيير شرط التحقق يتطلب تعديل خمسة أماكن، وهذا يزيد من احتمالية نسيان أحدها أو إدخال خطأ جديد. لكن المشكلة الأكبر هي أن التكرار يجعل الكود أكثر عرضة للـ Memory Leaks والـ Performance Issues، خاصة عندما يتعلق الأمر بـ Side Effects مثل الـ API Calls أو الـ Event Listeners.
لنأخذ مثالاً عملياً من مشروع حقيقي: في منصة تعليمية كنا نبنيها، كان لدينا مكونات متعددة تحتاج إلى جلب بيانات من API بناءً على معرف المستخدم. كل مكون كان يستخدم useEffect مع نفس الـ Dependency Array، ونفس منطق التحميل، ونفس معالجة الأخطاء. المشكلة ظهرت عندما قررنا تغيير طريقة التعامل مع الأخطاء: بدلاً من عرض رسالة خطأ عامة، أردنا عرض رسائل مخصصة لكل نوع خطأ. هذا التغيير البسيط تطلب تعديل ١٢ مكون مختلف، وكل تعديل كان يتطلب اختباراً منفصلاً للتأكد من عدم حدوث انهيار في مكان آخر. هذا النوع من التبعية المتبادلة بين المكونات هو ما يجعل المشاريع الكبيرة تتحول إلى مستنقع من الكود الصعب الصيانة.
الحل هنا هو استخراج هذا المنطق المتكرر إلى Custom Hook يمكن استخدامه في أي مكون. على سبيل المثال، بدلاً من كتابة نفس الكود لجلب البيانات في كل مكون، يمكننا إنشاء hook مخصص يسمى useFetchData. هذا الـ Hook لن يكون مجرد غلاف حول useEffect، بل سيحتوي على منطق ذكي للتعامل مع حالات مختلفة مثل التحميل، الأخطاء، وإعادة المحاولة عند الفشل. الميزة الحقيقية هنا هي أن أي تغيير في منطق جلب البيانات سيتم تطبيقه مرة واحدة فقط في الـ Hook، وسيتم عكسه تلقائياً على جميع المكونات التي تستخدمه.
// useFetchData.js
import { useState, useEffect } from 'react';
const useFetchData = (url, opti {}) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [retryCount, setRetryCount] = useState(0);
const maxRetries = options.maxRetries || 3;
useEffect(() => {
let isMounted = true;
const controller = new AbortController();
const { signal } = controller;
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url, { ...options, signal });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (isMounted) {
setData(result);
setError(null);
}
} catch (err) {
if (isMounted) {
if (retryCount < maxRetries && err.name !== 'AbortError') {
setTimeout(() => {
setRetryCount(prev => prev + 1);
}, 1000 * Math.pow(2, retryCount));
} else {
setError(err.message);
}
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
fetchData();
return () => {
isMounted = false;
controller.abort();
};
}, [url, retryCount]);
return { data, loading, error };
};
export default useFetchData;هذا الـ Hook ليس مجرد غلاف بسيط حول fetch، بل يحتوي على منطق متقدم للتعامل مع حالات معقدة مثل إلغاء الطلب عند إلغاء تحميل المكون (باستخدام AbortController)، وإعادة المحاولة تلقائياً عند الفشل مع تأخير متزايد (Exponential Backoff)، والتأكد من عدم تحديث الحالة بعد إلغاء تحميل المكون (باستخدام isMounted). هذه التفاصيل الصغيرة هي ما يميز الـ Hook الجيد عن مجرد دالة مساعدة. لاحظ أيضاً أننا استخدمنا signal من AbortController لإلغاء الطلب إذا تم إلغاء تحميل المكون، وهذا يمنع حدوث الـ Memory Leaks التي تحدث عندما يحاول الـ useEffect تحديث حالة المكون بعد إلغاء تحميله.
في أحد مشاريع لوحة التحكم الإدارية التي عملت عليها، كان لدينا مكون معقد لإدارة المستخدمين مع ميزات مثل البحث، الفلترة، والترتيب. المشكلة كانت أن منطق إدارة الحالة لهذا المكون كان يتجاوز ٣٠٠ سطر، وكان من الصعب جداً فهمه أو تعديله. الحل كان في تقسيم هذا المنطق إلى Custom Hooks صغيرة ومتخصصة، كل منها مسؤول عن جزء محدد من الوظائف. على سبيل المثال، أنشأنا hook يسمى useUserFiltering للتعامل مع منطق الفلترة والترتيب، وآخر يسمى useUserSearch للتعامل مع منطق البحث، وثالث يسمى usePagination للتعامل مع منطق التصفح.
الفائدة الحقيقية هنا ليست فقط في تقليل حجم المكون، بل في جعل الكود أكثر قابلية للاختبار والصيانة. كل hook يمكن اختباره بشكل مستقل، ويمكن تعديل منطق الفلترة مثلاً دون الحاجة إلى لمس منطق البحث أو التصفح. هذا النهج يتبع مبدأ الفصل بين الاهتمامات (Separation of Concerns)، وهو ما يجعل الكود أكثر مرونة وقابلية للتطوير على المدى الطويل. بالإضافة إلى ذلك، فإن استخدام Custom Hooks يجعل المكون الرئيسي أكثر نظافة وسهولة في الفهم، حيث يمكن للمطور الجديد أن يرى بوضوح ما يفعله المكون دون الحاجة للغوص في تفاصيل المنطق المعقد.
// useUserFiltering.js
import { useState, useEffect } from 'react';
const useUserFiltering = (users, initialFilters = {}) => {
const [filters, setFilters] = useState(initialFilters);
const [filteredUsers, setFilteredUsers] = useState(users);
useEffect(() => {
let result = [...users];
if (filters.role) {
result = result.filter(user => user.role === filters.role);
}
if (filters.status) {
result = result.filter(user => user.status === filters.status);
}
if (filters.sortBy) {
result.sort((a, b) => {
if (a[filters.sortBy] < b[filters.sortBy]) return filters.sortDirection === 'asc' ? -1 : 1;
if (a[filters.sortBy] > b[filters.sortBy]) return filters.sortDirection === 'asc' ? 1 : -1;
return 0;
});
}
setFilteredUsers(result);
}, [users, filters]);
const updateFilter = (key, value) => {
setFilters(prev => ({ ...prev, [key]: value }));
};
return { filteredUsers, filters, updateFilter };
};
export default useUserFiltering;هذا الـ Hook يأخذ قائمة المستخدمين وحالة الفلترة الأولية، ويعيد قائمة المستخدمين المفلترة بناءً على الفلترة الحالية. لاحظ كيف أننا استخدمنا useEffect لمراقبة التغييرات في users وfilters، وإعادة حساب القائمة المفلترة عند حدوث أي تغيير. هذا النمط شائع في Custom Hooks التي تتعامل مع البيانات، حيث نريد إعادة حساب النتيجة عند تغيير المدخلات. الميزة هنا هي أن المكون الذي يستخدم هذا الـ Hook لا يحتاج إلى معرفة كيفية عمل منطق الفلترة، بل فقط يحتاج إلى استدعاء updateFilter عند تغيير الفلترة، والحصول على القائمة المفلترة جاهزة للاستخدام.
على الرغم من أن Custom Hooks تبدو بسيطة في المفهوم، إلا أن هناك العديد من الفخاخ التي يمكن أن تقع فيها إذا لم تكن حذراً. أول هذه الفخاخ هو استخدام الـ Hooks داخل شروط أو حلقات، وهو ما يخالف قواعد React Hooks. على سبيل المثال، إذا كتبت كوداً مثل هذا:
if (condition) {
const [state, setState] = useState(initialValue);
}فإن React ستعطيك خطأ لأن ترتيب استدعاء الـ Hooks يجب أن يكون ثابتاً في كل مرة يتم فيها استدعاء المكون. هذا يعني أنه لا يمكنك استدعاء useState أو useEffect داخل if أو for أو أي بنية شرطية أو تكرارية. الحل هنا هو دائماً استدعاء الـ Hooks في المستوى الأعلى من الـ Hook الخاص بك، وتطبيق الشروط داخل الـ Hook إذا لزم الأمر. على سبيل المثال، بدلاً من كتابة:
const useC (condition) => {
if (condition) {
const [state, setState] = useState('');
}
}يمكنك كتابة:
const useC (condition) => {
const [state, setState] = useState(condition ? '' : null);
}فخ آخر شائع هو مشكلة الـ Closure في useEffect. تحدث هذه المشكلة عندما تعتمد على قيمة متغيرة داخل useEffect، لكن القيمة التي يلتقطها الـ Closure تكون قديمة. على سبيل المثال، إذا كتبت كوداً مثل هذا:
useEffect(() => {
const timer = setTimeout(() => {
console.log(count); // قد لا يكون القيمة الحالية لـ count
}, 1000);
return () => clearTimeout(timer);
}, [count]);فإن القيمة التي يطبعها setTimeout قد لا تكون القيمة الحالية لـ count، خاصة إذا تغير count بسرعة. هذا يحدث لأن الـ Closure داخل setTimeout يلتقط قيمة count عند إنشاء الـ Closure، وليس عند تنفيذ الكود داخل setTimeout. الحل هنا هو استخدام ref لتتبع القيمة الحالية:
const useCountLogger = () => {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
useEffect(() => {
const timer = setTimeout(() => {
console.log(countRef.current); // القيمة الحالية دائماً
}, 1000);
return () => clearTimeout(timer);
}, []);
}هذا النمط مفيد جداً في Custom Hooks التي تتعامل مع الـ Timers أو الـ Event Listeners، حيث نريد دائماً الوصول إلى أحدث قيمة للمتغير دون الحاجة إلى إضافة هذا المتغير إلى الـ Dependency Array الخاص بـ useEffect.
على الرغم من أن Custom Hooks هي أداة قوية، إلا أنها ليست الحل الأمثل لكل مشكلة. هناك حالات يكون فيها استخدام Custom Hook مبالغاً فيه ويزيد من تعقيد الكود دون داعٍ. على سبيل المثال، إذا كان لديك منطق بسيط جداً يستخدم مرة أو مرتين فقط في تطبيقك، فقد يكون من الأفضل الاحتفاظ به داخل المكون بدلاً من استخراجه إلى Custom Hook. القاعدة العامة هنا هي: إذا كان المنطق معقداً بما يكفي ليصعب فهمه عند النظر إليه داخل المكون، أو إذا كان يستخدم في أكثر من مكون، فمن الأفضل استخراجه إلى Custom Hook.
هناك أيضاً حالات يكون فيها استخدام Custom Hook غير مناسب بسبب طبيعة المشكلة. على سبيل المثال، إذا كان لديك منطق يعتمد بشكل كبير على DOM أو يتطلب الوصول المباشر إلى عناصر الصفحة، فقد يكون من الأفضل استخدام refs داخل المكون بدلاً من محاولة استخراج هذا المنطق إلى Custom Hook. هذا لأن Custom Hooks مصممة للعمل مع حالة React والمنطق، وليس مع DOM مباشرة. بالإضافة إلى ذلك، إذا كان المنطق يتطلب تفاعلاً مع مكتبات خارجية لا تدعم React، فقد يكون من الصعب دمجها داخل Custom Hook دون إضافة تعقيد غير ضروري.
لنفترض أن لديك مكوناً يحتاج إلى تبديل حالة بسيطة بين true وfalse عند الضغط على زر. هذا المنطق بسيط جداً ويمكن كتابته مباشرة داخل المكون:
const ToggleButton = () => {
const [isActive, setIsActive] = useState(false);
const toggle = () => {
setIsActive(prev => !prev);
};
return (
<button {toggle}>
{isActive ? 'Active' : 'Inactive'}
</button>
);
}في هذه الحالة، استخراج هذا المنطق إلى Custom Hook مثل useToggle لن يضيف أي قيمة، بل سيجعل الكود أكثر تعقيداً دون داعٍ. القاعدة هنا هي: لا تستخدم Custom Hook إلا إذا كان هناك فائدة حقيقية في إعادة استخدام المنطق أو إذا كان المنطق معقداً بما يكفي ليصعب فهمه داخل المكون.
بعد سنوات من بناء تطبيقات React مع فرق مختلفة، هذه هي النصائح الذهبية التي أتمنى أن يعرفها كل مطور قبل كتابة أول Custom Hook له:
وأخيراً، تذكر أن الهدف من Custom Hooks ليس فقط تقليل تكرار الكود، بل جعل الكود أكثر قابلية للفهم والصيانة. إذا وجدت نفسك تكتب Custom Hook معقد جداً لدرجة أنه يصعب فهمه، فمن الأفضل تقسيمه إلى عدة Hooks أصغر وأكثر تخصصاً. في عالم البرمجة، البساطة غالباً ما تكون مفتاح النجاح على المدى الطويل، وCustom Hooks هي أداة رائعة لتحقيق هذه البساطة عندما تستخدم بشكل صحيح.