اكتشف كيف تحول Custom Hooks في React من منطق متكرر إلى وحدات ذكية قابلة لإعادة الاستخدام، مع أمثلة حقيقية من مشاريع الإنتاج وتجنب الفخاخ الشائعة التي تكلف الفرق ساعات من Debugging.
في مشروع متوسط الحجم، وجدت نفسي أكتب نفس الكود تقريباً في خمسة مكونات مختلفة: منطق جلب البيانات من API مع معالجة الأخطاء وإدارة حالة التحميل. ليس فقط كان هذا مضيعة للوقت، بل أدى إلى أخطاء متكررة عندما نسيت تحديث منطق واحد من المكونات بعد تغيير الـ endpoint. المشكلة الحقيقية ليست في التكرار نفسه، بل في أن كل نسخة من هذا الكود كانت تعيش في عزلة، مما يجعل الصيانة كابوساً حقيقياً. هنا تأتي Custom Hooks كحل ثوري، لكنها ليست مجرد أداة تنظيمية - إنها طريقة لإعادة التفكير في كيفية بناء منطق التطبيق في React.
عندما نتحدث عن Custom Hooks، لا نتحدث فقط عن إعادة استخدام الكود، بل عن إعادة استخدام الحالة والسلوك. الفرق كبير: بينما يمكن لـ helper function إعادة استخدام منطق حسابي، فإن Custom Hook يعيد استخدام دورة حياة كاملة - الـ state، الـ effects، وحتى الـ refs. هذا يعني أنك تستطيع نقل منطق معقد مثل إدارة الـ WebSocket أو تتبع الـ scroll position من مكون إلى آخر دون تغيير سطر واحد من الكود الأصلي. لكن هناك ثمن يجب دفعه: فهم عميق لكيفية عمل React تحت الغطاء.
في بداية استخدام React، اعتدنا على كتابة دوال مساعدة مثل formatDate أو calculateTotal لنقل المنطق المتكرر خارج المكونات. هذه الطريقة جيدة للمهام البسيطة، لكنها تفشل عندما يتعلق الأمر بمنطق يعتمد على حالة أو تأثيرات جانبية. مثلاً، إذا كان لديك مكون يعرض قائمة منتجات مع فلترة حسب السعر، قد تكتب دالة filterProducts تأخذ قائمة المنتجات ومعايير الفلترة. لكن ماذا لو احتجت أيضاً إلى تتبع حالة الفلترة في الـ URL أو إرسال حدث تحليلات عند تغيير الفلترة؟ هنا تصبح الدالة المساعدة غير كافية.
Custom Hooks تحل هذه المشكلة لأنها تستطيع إدارة الحالة والتأثيرات بنفسها. بدلاً من تمرير state كمعاملات للدوال، فإن الـ Hook يحتوي على الـ state الخاص به ويتعامل مع التحديثات بشكل داخلي. هذا يعني أنك تستطيع كتابة منطق مثل 'عندما يتغير فلتر السعر، قم بجلب البيانات الجديدة وأرسل حدث تحليلات' مرة واحدة فقط، ثم تستخدم هذا المنطق في أي مكون دون القلق بشأن التفاصيل الداخلية. لكن الأهم من ذلك هو أن الـ Hook يصبح وحدة مستقلة يمكن اختبارها بشكل منعزل، مما يحسن جودة الكود بشكل كبير.
// مثال تقليدي: دالة مساعدة سطحية
function filterProducts(products, minPrice, maxPrice) {
return products.filter(p => p.price >= minPrice && p.price <= maxPrice);
}
// نفس المنطق باستخدام Custom Hook - إعادة استخدام أعمق
function useProductFilter(initialProducts) {
const [products, setProducts] = useState(initialProducts);
const [filters, setFilters] = useState({ minPrice: 0, maxPrice: 1000 });
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchFilteredProducts = async () => {
setIsLoading(true);
try {
const resp await fetch(`/api/products?min=${filters.minPrice}&max=${filters.maxPrice}`);
const data = await response.json();
setProducts(data);
// إرسال حدث تحليلات
analytics.track('filter_changed', filters);
} finally {
setIsLoading(false);
}
};
fetchFilteredProducts();
}, [filters]);
return { products, filters, setFilters, isLoading };
}في أحد المشاريع التي عملت عليها لشركة تجارة إلكترونية، كان لدينا صفحة بحث مع فلترة متقدمة تحتوي على أكثر من عشرة حقول. المشكلة الرئيسية كانت الأداء: كل ضغطة زر كانت تؤدي إلى إرسال طلب API جديد، مما يسبب تحميلاً زائداً على السيرفر وتجربة مستخدم بطيئة. الحل التقليدي كان استخدام setTimeout مع clearTimeout، لكن هذا المنطق كان متكرراً في ثلاثة مكونات مختلفة. بدلاً من ذلك، قمنا ببناء Custom Hook اسمه useSearchWithDebounce يدير حالة البحث مع debouncing تلقائي.
الجميل في هذا الحل هو أنه ليس مجرد إعادة استخدام للكود، بل إعادة استخدام لمنطق كامل مع حالة داخلية. الـ Hook يدير الـ state الخاص بالبحث، ويطبق الـ debouncing، ويتعامل مع الأخطاء، وحتى يعرض حالة التحميل. وعندما احتجنا لإضافة ميزة جديدة مثل 'حفظ البحث الأخير'، كان علينا تعديل مكان واحد فقط بدلاً من ثلاثة مكونات. هذا هو الفرق الحقيقي الذي تحدثه Custom Hooks: إنها تحول منطق متكرر إلى وحدات ذكية مستقلة.
function useSearchWithDebounce(initialValue = '', delay = 500) {
const [value, setValue] = useState(initialValue);
const [debouncedValue, setDebouncedValue] = useState(initialValue);
const [isSearching, setIsSearching] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
useEffect(() => {
if (debouncedValue) {
const search = async () => {
setIsSearching(true);
setError(null);
try {
const results = await api.search(debouncedValue);
// سيتم التعامل مع النتائج في المكون الذي يستخدم هذا Hook
} catch (err) {
setError(err.message);
} finally {
setIsSearching(false);
}
};
search();
}
}, [debouncedValue]);
return { value, setValue, debouncedValue, isSearching, error };
}
// استخدام Hook في مكون
function SearchComponent() {
const { value, setValue, isSearching, error } = useSearchWithDebounce();
return (
<div>
<input
value={value}
{(e) => setValue(e.target.value)}
placeholder="ابحث عن المنتجات..."
/>
{isSearching && <Spinner />}
{error && <ErrorMessage message={error} />}
</div>
);
}عندما تستخدم Custom Hook، فأنت في الواقع تنشئ نسخة مستقلة من حالة الـ Hook لكل مكون يستخدمه. هذا يعني أنه إذا كان لديك مكونان يستخدمان useSearchWithDebounce، فكل منهما سيكون له نسخة خاصة به من الـ state و الـ effects. هذا السلوك يختلف عن الدوال العادية التي تشارك نفس النطاق عند استدعائها عدة مرات. السبب في ذلك هو أن React تستخدم الـ Fiber architecture لتتبع حالة كل مكون على حدة، وعندما تستدعي useState داخل Hook، فإن React تربط هذه الحالة بالمكون الذي استدعى الـ Hook أصلاً.
المشكلة الحقيقية تظهر عندما تنسى هذا السلوك وتبدأ في مشاركة الحالة بين المكونات عن طريق الخطأ. مثلاً، إذا وضعت متغير خارج نطاق الـ Hook وحاولت استخدامه لتخزين حالة مشتركة، فستجد أن جميع المكونات التي تستخدم هذا Hook تشارك نفس الحالة. هذا يمكن أن يؤدي إلى سلوك غير متوقع ويجعل الـ Debugging كابوساً. الحل هو دائماً استخدام الـ state الداخلي للـ Hook وعدم الاعتماد على متغيرات خارجية إلا إذا كانت ثابتة تماماً (مثل الثوابت).
// ❌ خطأ شائع: مشاركة الحالة بين المكونات
let sharedValue = '';
function useBadSharedState() {
const [value, setValue] = useState(sharedValue);
// هذا سيؤدي إلى سلوك غير متوقع
return [value, setValue];
}
// ✅ الطريقة الصحيحة: كل مكون له حالته الخاصة
function useGoodState(initialValue) {
const [value, setValue] = useState(initialValue);
return [value, setValue];
}أحد أقوى استخدامات Custom Hooks هو إدارة الـ side effects المعقدة التي تتطلب تنظيفاً دقيقاً. مثلاً، في مشروع للوسائط المتعددة، احتجنا إلى إدارة اتصال WebSocket مع إعادة الاتصال التلقائي عند فقدان الاتصال. المشكلة كانت أن كل مرة يتم فيها إعادة الاتصال، كنا نخاطر بإنشاء اتصال جديد دون إغلاق القديم، مما يؤدي إلى تسرب الذاكرة. الحل كان بناء Custom Hook اسمه useWebSocket يدير الاتصال بالكامل مع منطق إعادة الاتصال والتنظيف التلقائي
ما يميز هذا الحل هو أنه يحول منطق معقد إلى واجهة بسيطة: أنت تمرر عنوان الـ WebSocket فقط، والـ Hook يعطيك الرسالة الأخيرة وحالة الاتصال ودالة لإرسال الرسائل. كل التفاصيل الداخلية مثل إعادة الاتصال وإدارة الـ event listeners مخفية داخل الـ Hook. هذا يجعل الكود أكثر قابلية للصيانة ويقلل من فرص الأخطاء. بالإضافة إلى ذلك، لأن الـ Hook يدير الـ cleanup تلقائياً في useEffect، فلا داعي للقلق بشأن تسرب الذاكرة عند إلغاء تحميل المكون.
function useWebSocket(url) {
const [lastMessage, setLastMessage] = useState(null);
const [connectionStatus, setConnectionStatus] = useState('connecting');
const [socket, setSocket] = useState(null);
const rec useRef(0);
const maxReconnectAttempts = 5;
const sendMessage = useCallback((message) => {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(message));
}
}, [socket]);
useEffect(() => {
const ws = new WebSocket(url);
setSocket(ws);
ws.onopen = () => {
setConnectionStatus('open');
reconnectAttempts.current = 0;
};
ws.onmessage = (event) => {
setLastMessage(JSON.parse(event.data));
};
ws.onclose = () => {
setConnectionStatus('closed');
if (reconnectAttempts.current < maxReconnectAttempts) {
const timeout = Math.min(1000 * Math.pow(2, reconnectAttempts.current), 30000);
setTimeout(() => {
reconnectAttempts.current++;
setConnectionStatus('reconnecting');
}, timeout);
}
};
ws.onerror = () => {
setConnectionStatus('error');
};
return () => {
ws.close();
};
}, [url]);
return { lastMessage, connectionStatus, sendMessage };
}
// استخدام في المكون
function ChatComponent() {
const { lastMessage, connectionStatus, sendMessage } = useWebSocket('wss://example.com/chat');
return (
<div>
<p>Status: {connectionStatus}</p>
<ul>
<li>{lastMessage?.text}</li>
</ul>
<button onClick={() => sendMessage({ text: 'Hello' })}>Send</button>
</div>
);
}أول فخ يقع فيه المطورون هو محاولة جعل الـ Custom Hook يفعل كل شيء. مثلاً، قد تحاول بناء hook واحد يدير الحالة، ويجلب البيانات، ويتعامل مع الـ UI في نفس الوقت. هذا يؤدي إلى hooks ضخمة يصعب صيانتها واختبارها. القاعدة الذهبية هي أن يكون كل hook مسؤولاً عن شيء واحد فقط، تماماً مثل المكونات. إذا وجدت نفسك تضيف الكثير من الـ props أو الـ return values للـ hook، فهذا إشارة إلى أنك بحاجة لتقسيمه إلى hooks أصغر وأكثر تركيزاً.
فخ آخر هو تجاهل الـ dependency array في useEffect. عندما يكون لديك effect يعتمد على قيمة من الـ props أو الـ state، يجب إضافتها إلى الـ dependency array. إذا نسيت ذلك، قد ينتهي بك الأمر باستخدام قيمة قديمة داخل الـ effect، مما يؤدي إلى سلوك غير متوقع. المشكلة الأكبر هي أن هذا النوع من الأخطاء قد لا يظهر فوراً، بل فقط في حالات معينة، مما يجعل الـ debugging صعباً جداً. دائماً استخدم eslint-plugin-react-hooks لمساعدتك في اكتشاف هذه الأخطاء قبل حدوثها.
على الرغم من قوة Custom Hooks، هناك حالات يكون فيها استخدامها غير مناسب. مثلاً، إذا كان لديك منطق بسيط جداً لا يعتمد على حالة أو تأثيرات، فقد يكون من الأسهل استخدام دالة مساعدة عادية. أيضاً، إذا كان الـ Hook سيتطلب الكثير من الـ props لتمريرها من المكون، فقد يكون من الأفضل بناء مكون مخصص بدلاً من ذلك. مثلاً، إذا كان لديك منطق عرض معقد يعتمد على عدة حالات، قد يكون من الأفضل بناء مكون <DataTable> بدلاً من محاولة إدخاله في hook.
حالة أخرى يجب تجنب Custom Hooks فيها هي عندما تحتاج إلى مشاركة حالة بين مكونات غير مرتبطة. في هذه الحالة، قد يكون من الأفضل استخدام Context API أو مكتبة إدارة حالة مثل Redux أو Zustand. الـ Hooks ليست مصممة لمشاركة الحالة بين المكونات، بل لإعادة استخدام المنطق داخل مكون واحد. إذا وجدت نفسك تحاول بناء hook لإدارة حالة عامة، فهذا إشارة إلى أنك بحاجة إلى حل آخر.
// ❌ حالة غير مناسبة: منطق بسيط جداً
function useAddNumbers() {
const add = (a, b) => a + b;
return add;
}
// ✅ أفضل: دالة مساعدة عادية
function addNumbers(a, b) {
return a + b;
}
// ❌ حالة غير مناسبة: مشاركة حالة بين مكونات
function useSharedCounter() {
const [count, setCount] = useState(0);
return [count, setCount];
}
// ✅ أفضل: استخدام Context API
const CounterC createContext();
function CounterProvider({ children }) {
const [count, setCount] = useState(0);
return (
<CounterContext.Provider value={{ count, setCount }}>
{children}
</CounterContext.Provider>
);
}بعد بناء عشرات الـ Custom Hooks في مشاريع إنتاجية، هذه هي النصائح التي أتمنى أن أعرفها منذ البداية: ابدأ دائماً بكتابة المنطق داخل المكون أولاً، ثم استخرجه إلى hook عندما تلاحظ أنك تكرر نفس الكود في مكانين. لا تحاول بناء hooks معقدة من البداية - دع الحاجة تقود التصميم. استخدم TypeScript مع الـ Hooks لتجنب الأخطاء عند استخدام الـ Hook في مكونات مختلفة. وأخيراً، لا تنسَ أن تختبر الـ Hook بشكل مستقل - فالـ Hook الجيد يجب أن يعمل حتى خارج مكون React.
الـ Custom Hooks ليست مجرد ميزة جميلة في React، بل هي طريقة لإعادة التفكير في كيفية بناء التطبيقات. عندما تستخدمها بشكل صحيح، فإنها تحول الكود من مجموعة من المكونات المتكررة إلى نظام من الوحدات الذكية المستقلة. لكن تذكر: القوة تأتي مع المسؤولية. كل hook تضيفه إلى مشروعك هو عقد بينك وبين بقية الفريق، ويجب أن يكون هذا العقد واضحاً وبسيطاً قدر الإمكان. إذا وجدت نفسك تبني hook معقد جداً، توقف واسأل نفسك: هل هذا حقاً الحل الصحيح، أم أنني أحاول حل مشكلة في المكان الخطأ؟
الكود الجيد ليس الذي يعمل فقط، بل الذي يمكن فهمه وتعديله بسهولة بعد ستة أشهر. الـ Custom Hooks هي أداة قوية لتحقيق هذا الهدف، لكنها ليست عصا سحرية - إنها تتطلب تفكيراً وتصميماً دقيقاً.
— مطور React مجهول في فريق الإنتاج