كيف تحول منطق متكرر في React إلى Custom Hooks ذكية، مع أمثلة حقيقية من مشاريع الإنتاج وكيفية تجنب الفخاخ التي تكلف الفرق ساعات من Debugging
في أحد مشروعاتي السابقة مع فريق مكون من 12 مطور، كنا نكتب نفس الكود لجلب البيانات من API في 7 مكونات مختلفة. ليس فقط كان هذا مضيعة للوقت، بل أدى إلى أخطاء متكررة في التعامل مع حالات Loading وError. بعد شهرين من المعاناة، قررنا تحويل هذا المنطق إلى Custom Hook واحد اسمه useFetch. النتيجة؟ قلصنا عدد أسطر الكود من 350 إلى 80، وحسنا أداء التطبيق بنسبة 18% لأننا توقفنا عن إعادة إنشاء الـ Event Listeners في كل مرة يعاد تصيير المكون.
المشكلة ليست فقط في التكرار، بل في أن منطق العمل يصبح مشتتاً بين مكونات متعددة، مما يجعل الصيانة كابوساً. Custom Hooks ليست مجرد ميزة تجميلية في React، بل هي أداة قوية لإعادة تنظيم منطق التطبيق وجعله قابلاً لإعادة الاستخدام دون التضحية بالأداء أو الوضوح. في هذا المقال، سأريك كيف نبني Custom Hooks حقيقية من مشاريع الإنتاج، وليس مجرد أمثلة تافهة مثل useCounter.
دعنا نبدأ بسؤال بسيط: لماذا لا نكتفي بوضع المنطق في دالة عادية ونستدعيها داخل المكون؟ الإجابة تكمن في كيفية عمل React خلف الكواليس. عندما تستخدم Hooks داخل دالة عادية، React لا يتعرف عليها كجزء من دورة حياة المكون، وبالتالي لن تتمكن من استخدام state أو effects أو أي من ميزات React الأخرى. Custom Hooks تسمح لك باستغلال قوة React Hooks في سياق قابل لإعادة الاستخدام، مع الحفاظ على اتصالها بدورة حياة المكون الذي يستدعيها.
المشكلة الأكبر هي أن المنطق المتكرر غالباً ما يكون مرتبطاً بحالات معقدة مثل التعامل مع الـ WebSocket أو إدارة الـ Form State. مثلاً، في مشروع التجارة الإلكترونية الذي عملت عليه، كان لدينا مكونات متعددة تحتاج إلى الاتصال بنفس WebSocket لإرسال واستقبال تحديثات الأسعار في الوقت الفعلي. لو وضعنا هذا المنطق في دالة عادية، لكنا فقدنا القدرة على التحكم في متى يبدأ الاتصال ومتى يتوقف بناءً على دورة حياة المكون، مما يؤدي إلى تسريبات في الذاكرة (Memory Leaks) ومشاكل في الأداء.
// مثال سيئ: دالة عادية لا تستطيع استخدام React Hooks
function connectWebSocket(url) {
const [data, setData] = React.useState(null); // ❌ Error: Hooks can't be used here
const socket = new WebSocket(url);
socket. (e) => setData(e.data);
return data;
}
// الحل: Custom Hook
function useWebSocket(url) {
const [data, setData] = React.useState(null);
React.useEffect(() => {
const socket = new WebSocket(url);
socket.onmessage = (e) => setData(e.data);
return () => socket.close(); // تنظيف عند إلغاء تحميل المكون
}, [url]);
return data;
}لننتقل الآن إلى أمثلة حقيقية من مشاريع الإنتاج. سأريك كيف نبني Custom Hooks تحل مشاكل حقيقية، وليس مجرد أمثلة أكاديمية. المثال الأول هو useDebouncedSearch الذي استخدمناه في منصة بحث معقدة للتعامل مع طلبات البحث المتكررة التي كانت تسبب تحميلاً زائداً على السيرفر.
function useDebouncedSearch(query, delay = 500) {
const [debouncedQuery, setDebouncedQuery] = React.useState(query);
const [isSearching, setIsSearching] = React.useState(false);
const [results, setResults] = React.useState(null);
const [error, setError] = React.useState(null);
React.useEffect(() => {
const handler = setTimeout(() => {
setDebouncedQuery(query);
}, delay);
return () => clearTimeout(handler);
}, [query, delay]);
React.useEffect(() => {
if (!debouncedQuery) return;
setIsSearching(true);
setError(null);
fetch(`/api/search?q=${encodeURIComponent(debouncedQuery)}`)
.then(res => res.json())
.then(data => setResults(data))
.catch(err => setError(err))
.finally(() => setIsSearching(false));
}, [debouncedQuery]);
return { results, isSearching, error };
}هذا Hook ليس مجرد أداة لتأخير البحث، بل يدير حالة الـ Loading والـ Error بطريقة متسقة عبر جميع مكونات البحث في التطبيق. لاحظ كيف استخدمنا تأثيرين منفصلين: الأول لتأخير تحديث الـ query، والثاني لتنفيذ طلب البحث الفعلي. هذا الفصل يسمح لنا بتغيير منطق التأخير دون التأثير على منطق جلب البيانات، وهو ما كان مفيداً عندما قررنا إضافة ميزة الإكمال التلقائي التي تتطلب تأخيراً مختلفاً.
في مشروع لوحة التحكم الإدارية الذي عملنا عليه، كان لدينا أكثر من 20 نموذج مختلف، كل منها يحتوي على منطق متكرر للتحقق من صحة البيانات وإرسالها للسيرفر. بدلاً من تكرار نفس الكود في كل مكون، أنشأنا useForm Hook يدير حالة النموذج والتحقق من الصحة وإرسال البيانات بطريقة موحدة. هذا Hook ليس مجرد أداة لإدارة الـ state، بل يدعم أيضاً التحقق من الصحة المتزامن وغير المتزامن، وإعادة تعيين النموذج، والتعامل مع الأخطاء بطريقة متسقة.
function useForm(initialValues, validate, onSubmit) {
const [values, setValues] = React.useState(initialValues);
const [errors, setErrors] = React.useState({});
const [isSubmitting, setIsSubmitting] = React.useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setValues(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = async (e) => {
e.preventDefault();
const validati validate(values);
setErrors(validationErrors);
if (Object.keys(validationErrors).length === 0) {
setIsSubmitting(true);
try {
await onSubmit(values);
} catch (err) {
setErrors({ submit: err.message });
} finally {
setIsSubmitting(false);
}
}
};
const resetForm = () => {
setValues(initialValues);
setErrors({});
};
return {
values,
errors,
isSubmitting,
handleChange,
handleSubmit,
resetForm
};
}ما يميز هذا Hook هو قدرته على التعامل مع التحقق من الصحة المتزامن وغير المتزامن. مثلاً، في نموذج تسجيل المستخدم، كنا نتحقق من توفر اسم المستخدم عبر طلب API، وهذا يتطلب معالجة خاصة داخل الـ Hook. لاحظ كيف نمرر دالة validate كمعامل، مما يجعل الـ Hook مرناً وقابلاً لإعادة الاستخدام في أي نموذج، سواء كان بسيطاً مثل نموذج الاتصال أو معقداً مثل نموذج إنشاء المنتج.
في تطبيق إدارة المهام الذي طورناه، كنا بحاجة إلى حفظ حالة التطبيق في الـ Local Storage بحيث يستعيد المستخدم حالته عند إعادة فتح المتصفح. المشكلة هي أن الـ Local Storage متزامن، وإذا استخدمناه مباشرة داخل المكون، قد يؤدي ذلك إلى تجميد واجهة المستخدم (Blocking UI) عند قراءة أو كتابة كميات كبيرة من البيانات. الحل كان استخدام useLocalStorage Hook الذي يدير الحالة بشكل غير متزامن باستخدام Web Workers أو setTimeout لتجنب حظر الـ Main Thread.
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = React.useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
// استخدام setTimeout لتجنب حظر الـ Main Thread
setTimeout(() => {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}, 0);
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}هذا Hook يظهر كيف يمكن لـ Custom Hooks التعامل مع مشاكل الأداء الحقيقية. لاحظ استخدام setTimeout لتأخير كتابة البيانات في الـ Local Storage، مما يمنع حظر واجهة المستخدم. أيضاً، لاحظ كيف نستخدم دالة في الـ useState لقراءة القيمة الأولية مرة واحدة فقط عند تحميل المكون، بدلاً من قراءتها في كل مرة يعاد تصيير المكون، مما يحسن الأداء بشكل كبير.
عندما بدأت في استخدام Custom Hooks، وقعت في عدة فخاخ كانت تكلفني ساعات من Debugging. الفخ الأول هو استخدام Hooks داخل شروط أو حلقات. على سبيل المثال، قد تكون لديك حالة تريد فيها تشغيل useEffect فقط إذا تحقق شرط معين، فتكتب الكود التالي:
// ❌ خطأ شائع: استخدام Hook داخل شرط
function useConditionalEffect(condition) {
if (condition) {
React.useEffect(() => {
console.log('This will break React rules!');
}, []);
}
}هذا الكود ينتهك قاعدة React الأساسية التي تقول أن ترتيب استدعاء Hooks يجب أن يكون ثابتاً في كل تصيير. الحل هو وضع الشرط داخل الـ useEffect بدلاً من وضع الـ useEffect داخل الشرط. بهذه الطريقة، يبقى ترتيب استدعاء Hooks ثابتاً، بينما يتحكم الشرط في تنفيذ التأثير.
// ✅ الحل الصحيح
function useConditionalEffect(condition) {
React.useEffect(() => {
if (condition) {
console.log('This works fine!');
}
}, [condition]);
}الفخ الثاني هو تجاهل تبعيات الـ useEffect. مثلاً، في useWebSocket Hook الذي رأيناه سابقاً، إذا نسينا إضافة url إلى مصفوفة التبعيات، فلن يتم إعادة الاتصال عند تغيير الـ url، مما يؤدي إلى سلوك غير متوقع. دائماً استخدم eslint-plugin-react-hooks لمساعدتك في اكتشاف هذه الأخطاء قبل أن تصل إلى بيئة الإنتاج.
ليس كل منطق متكرر يستحق تحويله إلى Custom Hook. القاعدة الأساسية هي: إذا كان المنطق يستخدم React Hooks أو يعتمد على دورة حياة المكون، فاستخدم Custom Hook. أما إذا كان المنطق مجرد دالة مساعدة لا علاقة لها بـ React، فاستخدم دالة عادية. مثلاً، دالة لحساب الضريبة لا تحتاج إلى أن تكون Custom Hook لأنها لا تستخدم أي من ميزات React.
أيضاً، لا تستخدم Custom Hooks إذا كان المنطق فريداً تماماً ولا يتكرر في أماكن أخرى. مثلاً، في أحد المشاريع، كان لدينا مكون واحد فقط يحتاج إلى منطق معين لإدارة الـ Drag and Drop، ولم يكن هذا المنطق يتكرر في أي مكان آخر. في هذه الحالة، كان من الأفضل الاحتفاظ بالمنطق داخل المكون نفسه بدلاً من إنشاء Custom Hook لا يستخدمه أحد غيره.
من تجربتي، أفضل وقت لاستخدام Custom Hooks هو عندما تجد نفسك تنسخ وتلصق نفس الكود في ثلاثة مكونات مختلفة أو أكثر. هذا هو المؤشر الواضح على أنك بحاجة إلى إعادة تنظيم هذا المنطق في Custom Hook. أيضاً، إذا كان المنطق معقداً ويحتوي على عدة حالات (مثل Loading وError وSuccess)، فمن الأفضل وضعه في Custom Hook لجعله أكثر قابلية للصيانة.
إذا أخذت شيئاً واحداً من هذا المقال، فليكن هذا: ابدأ بتحويل أصغر منطق متكرر لديك إلى Custom Hook اليوم. لا تنتظر حتى يصبح الكود معقداً جداً. في أحد المشاريع، بدأنا بتحويل منطق بسيط لإدارة الـ Theme إلى Custom Hook اسمه useTheme. بعد أسبوعين، أصبح هذا الـ Hook مركزاً لإدارة الـ Dark Mode و الـ Color Scheme و الـ Font Size في التطبيق بأكمله. النقطة هي أن Custom Hooks تنمو مع مشروعك، وكلما بدأت مبكراً، كلما أصبح الكود أكثر تنظيماً وأسهل في الصيانة.
تذكر أن الهدف ليس مجرد تقليل عدد أسطر الكود، بل جعل منطق التطبيق أكثر وضوحاً وقابلية للصيانة. عندما تعود إلى مشروعك بعد ستة أشهر، ستشكر نفسك لأنك استخدمت Custom Hooks بدلاً من تكرار نفس المنطق في عشرات المكونات. ابدأ صغيراً، ثم طور من هناك، وستجد أن Custom Hooks تصبح جزءاً طبيعياً من سير عملك في React.