اكتشف كيف تحول Custom Hooks في React من تكرار الكود الممل إلى منطق قابل لإعادة الاستخدام بذكاء، مع أمثلة حقيقية من مشاريع الإنتاج وتجنب الفخاخ الشائعة التي تكلف الفرق ساعات من التصحيح.
في أحد مشاريع الإنتاج الكبيرة لشركة ناشئة في مجال الصحة الرقمية، كان لدينا مكون واحد فقط مسؤول عن إدارة بيانات المريض: تحميل السجل الطبي، تحديث الحالة، وإرسال الإشعارات للمستشفى. بعد ثلاثة أشهر، أصبح هذا المكون ملفاً ضخماً يتجاوز ٨٠٠ سطر، وكل محاولة لتعديل أي جزء منه كانت تنتهي بكارثة: إما أن ينكسر شيء آخر، أو أن يتوقف الـ Event Loop عن الاستجابة لأننا نسينا أن نوقف الـ setInterval داخل useEffect. المشكلة لم تكن في الكود نفسه، بل في أننا كررنا نفس المنطق في خمسة مكونات مختلفة، وكل مرة ننسى تعديل شيء صغير، مثل تنظيف الـ listeners أو إدارة الـ loading state بشكل صحيح. هنا ظهرت الحاجة لـ Custom Hooks، ليس كخيار جميل، بل كضرورة هندسية لإنقاذ المشروع من الفوضى.
الـ Custom Hooks في React ليست مجرد طريقة لتنظيم الكود، بل هي أداة قوية لإعادة استخدام منطق الحالة والسلوك بطريقة تحافظ على أداء التطبيق وتقلل من الأخطاء البشرية. عندما تستخدم useState و useEffect داخل مكون، فأنت في الواقع تكتب منطقاً مرتبطاً بوظيفة محددة، ولكن عندما تنقل هذا المنطق إلى Custom Hook، فأنت تحول هذا الرابط إلى وحدة مستقلة يمكن إعادة استخدامها في أي مكان آخر. الفرق هنا ليس فقط في التنظيم، بل في كيفية تعامل React مع الذاكرة والمعالج: عندما تكرر نفس الكود في مكونات متعددة، فأنت تضاعف عدد الـ re-renders والـ effects التي تعمل في الخلفية، مما يؤدي إلى استهلاك غير ضروري للذاكرة والمعالج. أما مع Custom Hook، فأنت تضمن أن المنطق يعمل مرة واحدة فقط، حتى لو استخدمته في عشرة مكونات مختلفة.
الكثير من المطورين يعتقدون أن حل مشكلة تكرار الكود هو ببساطة استخراج الدوال المشتركة إلى ملف منفصل واستدعائها داخل المكونات. هذا النهج يعمل في بعض الحالات البسيطة، مثل حساب مجموع أو تنسيق تاريخ، لكنه يفشل فشلاً ذريعاً عندما يتعلق الأمر بمنطق الحالة أو الـ side effects. السبب هو أن الدوال العادية لا تستطيع استخدام الـ Hooks الخاصة بـ React، مثل useState أو useEffect، لأنها يجب أن تُستدعى داخل مكون وظيفي أو داخل Custom Hook آخر. هذا يعني أنك إذا حاولت استخراج منطق يحتوي على useState إلى دالة عادية، فستحصل على خطأ: "Invalid hook call. Hooks can only be called inside of the body of a function component."
الـ Custom Hooks تحل هذه المشكلة لأنها ببساطة دوال عادية تبدأ باسم use، مما يسمح لها باستخدام جميع الـ Hooks الخاصة بـ React. ولكن الفائدة الحقيقية ليست فقط في تجاوز هذا الخطأ، بل في كيفية تعامل React مع هذه الدوال خلف الكواليس. عندما تستخدم Custom Hook داخل مكون، فإن React يعامل كل استدعاء لهذا Hook ككيان مستقل، مما يعني أن كل مكون يحصل على نسخته الخاصة من الحالة والـ effects. هذا السلوك مشابه تماماً لكيفية عمل الـ Hooks الأصلية مثل useState، حيث أن كل استدعاء لـ useState داخل مكون يعطي هذا المكون حالة مستقلة عن المكونات الأخرى.
// مثال سيئ: دالة عادية لا تستطيع استخدام Hooks
function fetchData(url) {
// ❌ خطأ: لا يمكن استخدام useState هنا
const [data, setData] = useState(null);
useEffect(() => {
fetch(url).then(res => res.json()).then(setData);
}, [url]);
return data;
}
// مثال جيد: Custom Hook
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
setLoading(true);
fetch(url)
.then(res => res.json())
.then(data => {
if (isMounted) setData(data);
})
.catch(err => {
if (isMounted) setError(err);
})
.finally(() => {
if (isMounted) setLoading(false);
});
return () => {
isMounted = false;
};
}, [url]);
return { data, loading, error };
}في أحد مشاريع التجارة الإلكترونية التي عملت عليها، كان لدينا مكون بحث متقدم يسمح للمستخدمين بتصفية المنتجات بناءً على عدة معايير مثل السعر، التصنيف، والتقييم. المشكلة كانت في أن كل مرة يقوم المستخدم بتغيير أي فلتر، كنا نرسل طلباً جديداً للسيرفر، ولكن إذا قام المستخدم بتغيير الفلتر بسرعة قبل أن ينتهي الطلب السابق، كنا نحصل على نتائج غير متوقعة أو حتى خطأ في واجهة المستخدم لأن الـ state لم يكن متزامناً مع الاستجابة الأخيرة. الحل التقليدي كان استخدام علم مثل isLatest داخل useEffect، ولكن هذا الحل كان يتكرر في ثلاثة مكونات مختلفة، وكل مرة كنا ننسى تعديل شيء صغير، مثل تنظيف الـ abort controller أو إدارة الـ loading state بشكل صحيح.
الـ Custom Hook هنا كان الحل الأمثل لأنه سمح لنا بتوحيد منطق البحث في مكان واحد، وضمان أن كل مكون يستخدم نفس المنطق دون تكرار. الميزة الإضافية كانت في كيفية تعامل هذا Hook مع الـ Memory Leaks: باستخدام AbortController، كنا نستطيع إلغاء الطلبات السابقة قبل إرسال طلبات جديدة، مما يمنع تراكم الطلبات في الخلفية واستهلاك موارد غير ضروري. هذا النوع من التفاصيل الصغيرة هو ما يميز الكود الجيد عن الكود الذي يعمل فقط، وهو ما يجعل Custom Hooks أداة لا غنى عنها في المشاريع الكبيرة.
function useSearch(query, opti {}) {
const { debounceTime = 300, initialData = null } = options;
const [results, setResults] = useState(initialData);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const abortControllerRef = useRef(null);
useEffect(() => {
if (!query) {
setResults(initialData);
return;
}
const timer = setTimeout(() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
const abortController = new AbortController();
abortControllerRef.current = abortController;
setLoading(true);
setError(null);
fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: abortController.signal
})
.then(res => res.json())
.then(data => {
if (!abortController.signal.aborted) {
setResults(data);
}
})
.catch(err => {
if (err.name !== 'AbortError') {
setError(err);
}
})
.finally(() => {
if (!abortController.signal.aborted) {
setLoading(false);
}
});
}, debounceTime);
return () => {
clearTimeout(timer);
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, [query, debounceTime]);
return { results, loading, error };
}في الكود السابق، هناك ثلاث آليات رئيسية لمنع الـ Memory Leaks: أولاً، استخدام AbortController لإلغاء الطلبات السابقة قبل إرسال طلبات جديدة، مما يمنع تراكم الطلبات في الخلفية واستهلاك الذاكرة والمعالج بشكل غير ضروري. ثانياً، استخدام علم isMounted بشكل غير مباشر عبر التحقق من حالة الـ signal.aborted داخل الـ then و finally، مما يضمن أننا لا نقوم بتحديث الـ state بعد أن يكون المكون قد تم إلغاء تحميله. ثالثاً، تنظيف الـ timer و الـ abortController داخل الـ cleanup function الخاصة بـ useEffect، مما يضمن عدم ترك أي موارد معلقة بعد إلغاء تحميل المكون.
هذه التفاصيل الصغيرة هي ما يجعل الفرق بين كود يعمل وكود يعمل بكفاءة. في المشاريع الكبيرة، هذه الفروقات تتراكم وتؤدي إلى مشاكل أداء حقيقية، مثل بطء واجهة المستخدم أو حتى تجمد المتصفح. الـ Custom Hooks هنا لا توفر فقط إعادة استخدام الكود، بل تضمن أيضاً أن هذا الكود يعمل بكفاءة في جميع السيناريوهات، بما في ذلك الحالات الطرفية مثل إلغاء تحميل المكون بسرعة أو تغيير الـ dependencies بشكل متكرر.
في أحد مشاريع إدارة المهام التي عملت عليها، كان لدينا مشكلة شائعة: كيف نمرر بيانات المستخدم الحالية (مثل الاسم، الصورة، والإعدادات) إلى جميع المكونات دون الحاجة إلى تمريرها يدوياً عبر الـ props في كل مستوى من شجرة المكونات. الحل التقليدي كان استخدام Context API، ولكن المشكلة كانت في أن كل مكون يحتاج إلى استدعاء useContext للحصول على البيانات، مما يؤدي إلى إعادة تحميل المكونات بشكل غير ضروري عند تغيير أي قيمة في الـ Context، حتى لو لم تكن هذه القيمة ذات صلة بالمكون.
الحل الأمثل هنا كان دمج الـ Custom Hooks مع الـ Context API لإنشاء طبقة تجريدية تسمح للمكونات بالحصول على البيانات التي تحتاجها فقط، دون إعادة تحميل عند تغيير بيانات أخرى. الفكرة بسيطة: بدلاً من أن يستدعي كل مكون useContext مباشرة، نقوم بإنشاء Custom Hook مثل useUser الذي يقوم باستدعاء useContext داخلياً ويعيد فقط البيانات التي يحتاجها المكون. هذا النهج لا يقلل فقط من عدد الـ re-renders، بل يجعل الكود أكثر قابلية للصيانة لأنه يخفي تفاصيل تنفيذ الـ Context خلف واجهة بسيطة.
// UserContext.js
import { createContext, useContext } from 'react';
const UserC createContext(null);
export function UserProvider({ children, user }) {
return (
<UserContext.Provider value={user}>
{children}
</UserContext.Provider>
);
}
export function useUser() {
const user = useContext(UserContext);
if (user === null) {
throw new Error('useUser must be used within a UserProvider');
}
return user;
}
// في مكون آخر
function UserProfile() {
const user = useUser(); // يحصل فقط على البيانات التي يحتاجها
return <div>{user.name}</div>;
}عندما يستخدم المكون useContext مباشرة، فإنه يعيد كامل الـ value التي تم تمريرها إلى الـ Provider، مما يعني أن أي تغيير في أي جزء من هذه القيمة يؤدي إلى إعادة تحميل المكون، حتى لو كان التغيير في جزء غير مرتبط بالمكون. على سبيل المثال، إذا كان الـ Context يحتوي على كائن مثل { user: { name, avatar }, settings: { theme, language } }، فإن تغيير theme سيؤدي إلى إعادة تحميل جميع المكونات التي تستخدم هذا الـ Context، حتى تلك التي تحتاج فقط إلى بيانات المستخدم. أما مع Custom Hook مثل useUser، فإن المكون يحصل فقط على البيانات التي يحتاجها، مما يقلل من عدد الـ re-renders بشكل كبير.
بالإضافة إلى ذلك، فإن استخدام Custom Hook يجعل الكود أكثر قابلية للصيانة لأنه يخفي تفاصيل تنفيذ الـ Context خلف واجهة بسيطة. إذا قررت في المستقبل تغيير هيكلية الـ Context أو حتى استبداله بـ Zustand أو Redux، فلن تحتاج إلى تعديل جميع المكونات التي تستخدم البيانات، بل ستعدل فقط الـ Custom Hook. هذا النوع من التجريد هو ما يجعل الفرق بين الكود الذي يمكن صيانته والكود الذي يصبح كابوساً عند كل تعديل.
على الرغم من أن الـ Custom Hooks أداة قوية، إلا أنها ليست خالية من المشاكل. أحد أكثر الأخطاء شيوعاً هو محاولة استخدام Custom Hook داخل حلقة أو شرط، مما يؤدي إلى خطأ: "Rendered fewer hooks than expected." السبب هو أن React يعتمد على ترتيب استدعاء الـ Hooks لتحديد أي حالة أو effect ينتمي إلى أي استدعاء، فإذا قمت بتغيير عدد الـ Hooks بين الـ renders، فإن React سيفقد التزامن وسيؤدي ذلك إلى سلوك غير متوقع أو أخطاء.
// ❌ خطأ شائع: استخدام Custom Hook داخل شرط
function Component({ shouldFetch }) {
if (shouldFetch) {
const { data } = useFetch('https://api.example.com'); // خطأ
return <div>{data}</div>;
}
return null;
}
// ✅ الحل الصحيح: استدعاء الـ Hook دائماً وإدارة الشرط داخله
function Component({ shouldFetch }) {
const { data } = useFetch(shouldFetch ? 'https://api.example.com' : null);
return shouldFetch ? <div>{data}</div> : null;
}مشكلة أخرى شائعة هي تجاهل تنظيف الـ effects داخل الـ Custom Hooks، مما يؤدي إلى الـ Memory Leaks. على سبيل المثال، إذا كان الـ Hook يقوم بإعداد مستمع للأحداث أو فتح اتصال بـ WebSocket، فيجب أن يقوم بتنظيف هذه الموارد داخل الـ cleanup function الخاصة بـ useEffect. تجاهل هذا التنظيف يؤدي إلى تراكم المستمعين أو الاتصالات في الخلفية، مما يستهلك الذاكرة والمعالج بشكل غير ضروري وقد يؤدي إلى سلوك غير متوقع عند إلغاء تحميل المكون.
على الرغم من أن الـ Custom Hooks أداة قوية، إلا أنها ليست الحل الأمثل في جميع الحالات. إذا كان المنطق الذي تريد إعادة استخدامه بسيطاً جداً ولا يحتوي على حالة أو effects، فقد يكون من الأفضل استخدام دالة عادية بدلاً من Custom Hook. على سبيل المثال، إذا كنت تريد حساب مجموع عددين أو تنسيق تاريخ، فلا حاجة لاستخدام Custom Hook لأن الدوال العادية ستفي بالغرض دون إضافة تعقيد غير ضروري.
أيضاً، إذا كان المنطق الذي تريد إعادة استخدامه مرتبطاً بواجهة المستخدم بشكل وثيق، مثل مكون زر مخصص أو قائمة منسدلة، فقد يكون من الأفضل إنشاء مكون React عادي بدلاً من Custom Hook. السبب هو أن المكونات توفر مرونة أكبر في التحكم في الـ rendering والسلوك، بينما الـ Custom Hooks مخصصة لإعادة استخدام منطق الحالة والسلوك فقط. في النهاية، القرار يعتمد على طبيعة المشكلة التي تحاول حلها: إذا كانت المشكلة تتعلق بالحالة أو الـ side effects، فاستخدم Custom Hook، أما إذا كانت تتعلق بواجهة المستخدم، فاستخدم مكون.
إذا كنت تعمل على مشروع React يحتوي على أكثر من ثلاثة مكونات تشترك في نفس المنطق، فابدأ فوراً في التفكير في كيفية تحويل هذا المنطق إلى Custom Hook. لا تنتظر حتى يصبح الكود فوضوياً ويصعب صيانته، لأن إعادة البناء لاحقاً ستكون أصعب بكثير. تذكر أن الـ Custom Hooks ليست مجرد طريقة لتنظيم الكود، بل هي أداة هندسية تضمن أن منطق الحالة والسلوك يعمل بكفاءة في جميع السيناريوهات، بما في ذلك الحالات الطرفية مثل إلغاء تحميل المكون بسرعة أو تغيير الـ dependencies بشكل متكرر. ابدأ صغيراً، اختبر جيداً، واستمتع بكود أنظف وأسرع وأكثر قابلية للصيانة.