اكتشف كيف تحول Custom Hooks في React منطق المكونات إلى وحدات مستقلة وقابلة لإعادة الاستخدام، مع أمثلة حقيقية من مشاريع الإنتاج وكيفية تجنب الفخاخ الشائعة مثل تسرب الذاكرة وتداخل الـ Event Loops.
في أحد مشاريع الإنتاج الكبيرة التي عملت عليها، كان لدينا مكون واحد مسؤول عن جلب البيانات من ثلاثة مصادر مختلفة: API خارجي، WebSocket للبث اللحظي، وLocalStorage للتخزين المؤقت. المكون نما ليحتوي على أكثر من ٣٠٠ سطر من الكود، وأصبح من المستحيل تقريباً تتبع حالة الـ Loading أو الـ Error لكل مصدر. هنا ظهرت قوة Custom Hooks: بدلاً من تكرار نفس المنطق في كل مكون، استخرجنا كل مصدر إلى hook مستقل، وقمنا بإعادة استخدامه في ١٢ مكوناً مختلفاً دون تغيير سطر واحد في منطق الجلب. النتيجة؟ قلصنا حجم الكود بنسبة ٦٠٪، وأصبح تتبع الأخطاء أسهل بعشر مرات لأن كل hook يعمل في عزلة تامة.
المشكلة الحقيقية ليست في تكرار الكود فقط، بل في كيفية إدارة الحالة والـ Side Effects في مكونات React. عندما يكون لديك مكون واحد يتعامل مع الـ Form Validation، الـ API Calls، والـ Local Storage في نفس الوقت، فإن أي تغيير في منطق الـ Validation قد يكسر الـ API Call دون أن تدري. Custom Hooks ليست مجرد طريقة لتنظيم الكود، بل هي نمط تصميم يسمح بفصل المسؤوليات تماماً كما تفعل الـ Services في الـ Backend. الفرق هنا أنك لا تحتاج إلى مكتبة خارجية أو تعقيد إضافي، فقط React نفسه مع قليل من التفكير المعماري.
يصبح كابوساً عندما تريد تمرير props من HOC إلى مكون داخلي. بالإضافة إلى ذلك، إذا استخدمت أكثر من HOC على نفس المكون، ينتهي بك الأمر بمكون ملفوف في طبقات متعددة مثل البصل، ويصبح من الصعب جداً تتبع مصدر الـ props أو حتى تسمية المكون الناتج في أدوات المطور.
الـ Render Props كانت أفضل قليلاً، لكنها أضافت تعقيداً غير ضروري للكود. كان عليك تمرير دالة كـ children للمكون، وهذا يجعل الكود يبدو غير طبيعي ويصعب قراءته. أما Custom Hooks، فهي ببساطة دوال عادية تستخدم hooks أخرى داخلها. لا يوجد تعقيد إضافي، لا يوجد مكونات ملفوفة، فقط منطق مستقل يمكن استخدامه في أي مكان. والأفضل من ذلك، يمكنك استخدام أكثر من hook في نفس المكون دون أي مشكلة، وكل hook يعمل في نطاقه الخاص دون تداخل.
// مثال سيئ: HOC مع Prop Drilling
const withDataFetching = (WrappedComponent) => {
return (props) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData().then(data => {
setData(data);
setLoading(false);
});
}, []);
return <WrappedComponent data={data} loading={loading} {...props} />;
};
};
// مثال جيد: Custom Hook
const useDataFetching = (url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const c new AbortController();
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
if (err.name !== 'AbortError') {
setError(err);
setLoading(false);
}
});
return () => controller.abort();
}, [url]);
return { data, loading, error };
};
// الاستخدام في المكون
const MyComponent = () => {
const { data, loading, error } = useDataFetching('https://api.example.com/data');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{JSON.stringify(data)}</div>;
};عندما تنشئ Custom Hook، فأنت في الواقع تنشئ دالة عادية، لكن React يعاملها بطريقة خاصة بسبب اسمها الذي يبدأ بكلمة use. هذا ليس مجرد اصطلاح، بل هو جزء من آلية عمل React نفسها. عندما تستدعي useState أو useEffect داخل hook الخاص بك، فإن React يربط هذه الاستدعاءات بالمكون الذي يستخدم الـ hook، وليس بالـ hook نفسه. هذا يعني أن كل مكون يستخدم نفس الـ hook يحصل على نسخة مستقلة من الحالة والـ Side Effects، حتى لو كان الـ hook نفسه دالة واحدة.
لفهم هذا بشكل أعمق، دعنا نتحدث عن الـ Fiber Architecture في React. عندما تقوم React بعمل الـ Reconciliation للمكونات، فإنها تتبع شجرة المكونات وتحدد أي أجزاء تحتاج إلى إعادة رسم. عندما تستخدم Custom Hook، فإن React يربط الحالة والـ Effects الخاصة بالـ hook بالمكون الذي يستخدمه، وليس بالـ hook نفسه. هذا يعني أنه إذا كان لديك مكونان يستخدمان نفس الـ hook، فإن كل مكون يحصل على نسخة مستقلة من الحالة، حتى لو كان الـ hook نفسه دالة واحدة. هذا السلوك مشابه لكيفية عمل الـ Closures في JavaScript، لكن مع إضافة طبقة من التعقيد بسبب الـ Event Loop والـ Fiber Scheduler في React.
// مثال يوضح كيفية تعامل React مع Custom Hooks
const useCounter = (initialValue = 0) => {
const [count, setCount] = useState(initialValue);
const increment = () => {
// هنا React يربط setCount بالمكون الذي يستخدم useCounter
setCount(prev => prev + 1);
};
const decrement = () => {
setCount(prev => prev - 1);
};
return { count, increment, decrement };
};
// المكون الأول يستخدم useCounter
const Comp () => {
const { count, increment } = useCounter();
return <button onClick={increment}>Count: {count}</button>;
};
// المكون الثاني يستخدم نفس useCounter
const ComponentB = () => {
const { count, decrement } = useCounter(10);
return <button onClick={decrement}>Count: {count}</button>;
};
// كل مكون يحصل على نسخة مستقلة من الحالة
// ComponentA.count !== ComponentB.countفي مشروع إدارة المهام الذي عملت عليه لشركة ناشئة، كان لدينا مشكلة مع الـ Form Validation. كل نموذج كان يحتوي على منطق تحقق مكرر، وكان من الصعب جداً الحفاظ على اتساق الرسائل بين الـ Frontend والـ Backend. الحل؟ أنشأنا Custom Hook اسمه useFormValidation يأخذ الـ Schema من الـ Backend ويعيد دالة للتحقق من المدخلات. هذا Hook أصبح يستخدم في ١٨ نموذجاً مختلفاً، وأصبح لدينا مكان واحد لتحديث منطق الـ Validation بدلاً من تحديث كل نموذج على حدة.
مثال آخر من مشروع التجارة الإلكترونية: كان لدينا مكون ProductCard يعرض معلومات المنتج ويحتوي على زر إضافة إلى السلة. المشكلة كانت في أن منطق إضافة المنتج إلى السلة كان معقداً ويتضمن التحقق من المخزون، وإضافة المنتج إلى الـ Local Storage، وإرسال حدث إلى Google Analytics. بدلاً من وضع كل هذا المنطق في مكون ProductCard، أنشأنا hook اسمه useCart يضيف المنتج إلى السلة ويعيد حالة الـ Cart. هذا Hook أصبح يستخدم في ثلاثة مكونات مختلفة: ProductCard، وProductDetail، وQuickView.
// useCart Hook من مشروع التجارة الإلكترونية
const useCart = () => {
const [cart, setCart] = useState(() => {
const savedCart = localStorage.getItem('cart');
return savedCart ? JSON.parse(savedCart) : [];
});
const addToCart = (product, quantity = 1) => {
setCart(prevCart => {
const existingItem = prevCart.find(item => item.id === product.id);
let newCart;
if (existingItem) {
newCart = prevCart.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + quantity }
: item
);
} else {
newCart = [...prevCart, { ...product, quantity }];
}
// حفظ السلة في Local Storage
localStorage.setItem('cart', JSON.stringify(newCart));
// إرسال حدث إلى Google Analytics
if (window.gtag) {
gtag('event', 'add_to_cart', {
items: [{ id: product.id, name: product.name, quantity }]
});
}
return newCart;
});
};
const removeFromCart = (productId) => {
setCart(prevCart => {
const newCart = prevCart.filter(item => item.id !== productId);
localStorage.setItem('cart', JSON.stringify(newCart));
return newCart;
});
};
return { cart, addToCart, removeFromCart };
};
// استخدام Hook في مكون ProductCard
const ProductCard = ({ product }) => {
const { addToCart } = useCart();
return (
<div className="product-card">
<h3>{product.name}</h3>
<p>{product.price} $</p>
<button {() => addToCart(product)}>Add to Cart</button>
</div>
);
};في مشروع لوحة التحكم المالية الذي عملت عليه، كان لدينا حاجة لبث الأسعار اللحظية للعملات الرقمية. استخدمنا WebSocket للاتصال بالخادم، لكن المشكلة كانت في إدارة الاتصال وإعادة الاتصال عند انقطاع الشبكة. أنشأنا hook اسمه useWebSocket يعالج كل هذا المنطق المعقد ويعيد البيانات اللحظية فقط. هذا Hook أصبح يستخدم في أربعة مكونات مختلفة، وكلها تعرض نفس البيانات دون أي تكرار في منطق الاتصال.
// useWebSocket Hook للتعامل مع البث اللحظي
const useWebSocket = (url) => {
const [data, setData] = useState(null);
const [isConnected, setIsConnected] = useState(false);
const [error, setError] = useState(null);
const wsRef = useRef(null);
useEffect(() => {
const c () => {
const ws = new WebSocket(url);
ws.onopen = () => {
setIsConnected(true);
setError(null);
};
ws.onmessage = (event) => {
setData(JSON.parse(event.data));
};
ws.onerror = (event) => {
setError(event);
};
ws.onclose = () => {
setIsConnected(false);
// إعادة الاتصال بعد ٥ ثوان
setTimeout(connect, 5000);
};
wsRef.current = ws;
};
connect();
return () => {
if (wsRef.current) {
wsRef.current.close();
}
};
}, [url]);
return { data, isConnected, error };
};
// استخدام Hook في مكون Dashboard
const Dashboard = () => {
const { data, isConnected } = useWebSocket('wss://api.example.com/prices');
if (!isConnected) return <div>Connecting...</div>;
return (
<div>
<h2>Real-time Prices</h2>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
};أول فخ يقع فيه المطورون هو تجاهل تنظيف الـ Side Effects في Custom Hooks. عندما تستخدم useEffect داخل hook، يجب عليك دائماً إرجاع دالة للتنظيف. إذا لم تفعل ذلك، فقد ينتهي بك الأمر بـ Memory Leak أو اتصال مفتوح بعد إلغاء تحميل المكون. مثلاً، في hook useWebSocket الذي ذكرناه سابقاً، إذا لم نقم بإغلاق الاتصال عند إلغاء تحميل المكون، فإن الـ WebSocket سيبقى مفتوحاً حتى إغلاق الصفحة بالكامل، وهذا يستهلك موارد غير ضرورية.
فخ آخر هو الاعتماد على قيم خارجية دون إضافتها إلى قائمة الاعتمادات في useEffect. مثلاً، إذا كان لديك hook يستخدم قيمة من props دون إضافتها إلى قائمة الاعتمادات، فإن الـ Effect لن يعمل عند تغيير هذه القيمة. هذا خطأ شائع جداً ويؤدي إلى سلوك غير متوقع. الحل هو دائماً إضافة جميع القيم الخارجية التي يعتمد عليها الـ Effect إلى قائمة الاعتمادات، حتى لو كنت تعتقد أنها لن تتغير.
// مثال على خطأ شائع: عدم تنظيف الـ Side Effects
const useInterval = (callback, delay) => {
useEffect(() => {
const id = setInterval(callback, delay);
// ❌ نسيت إرجاع دالة للتنظيف
}, [callback, delay]);
};
// الحل الصحيح
const useIntervalFixed = (callback, delay) => {
useEffect(() => {
const id = setInterval(callback, delay);
return () => clearInterval(id); // ✅ تنظيف الـ Interval
}, [callback, delay]);
};
// مثال على خطأ آخر: الاعتماد على قيمة خارجية دون إضافتها إلى قائمة الاعتمادات
const useFetchWithDependencyError = (url) => {
const [data, setData] = useState(null);
const token = useAuth(); // قيمة خارجية
useEffect(() => {
fetch(url, { headers: { Authorization: token } })
.then(res => res.json())
.then(setData);
// ❌ token غير مضاف إلى قائمة الاعتمادات
}, [url]);
return data;
};
// الحل الصحيح
const useFetchWithDependencyFixed = (url) => {
const [data, setData] = useState(null);
const token = useAuth();
useEffect(() => {
fetch(url, { headers: { Authorization: token } })
.then(res => res.json())
.then(setData);
// ✅ token مضاف إلى قائمة الاعتمادات
}, [url, token]);
return data;
};إذا كنت تريد بناء Custom Hooks قوية وقابلة للصيانة، فاتبع هذه القاعدة الذهبية: اجعل كل hook مسؤولاً عن شيء واحد فقط. لا تجمع بين منطق الجلب ومنطق التخزين في نفس الـ hook، ولا تضع الـ Validation مع الـ API Calls. كلما كان الـ hook أصغر وأكثر تركيزاً، كان من الأسهل إعادة استخدامه وتحديثه دون كسر المكونات الأخرى. مثلاً، بدلاً من إنشاء hook واحد اسمه useProduct يفعل كل شيء، قم بإنشاء hooks صغيرة مثل useProductFetching، useProductValidation، وuseProductCart.
أيضاً، لا تخف من استخدام أكثر من hook في نفس المكون. في الواقع، هذا هو الهدف من Custom Hooks. إذا كان لديك مكون معقد، قم بتقسيمه إلى عدة hooks صغيرة، وكل hook يعالج جزء محدد من المنطق. هذا يجعل الكود أكثر قابلية للقراءة ويسهل اختبار كل جزء على حدة. وأخيراً، دائماً اختبر الـ hooks الخاصة بك بشكل مستقل باستخدام مكتبات مثل React Testing Library. لا تنتظر حتى تستخدم الـ hook في مكون لتكتشف الأخطاء، اختبره كما تختبر أي دالة عادية.
Custom Hooks ليست مجرد طريقة لتنظيم الكود، بل هي نمط تصميم يسمح بفصل المسؤوليات تماماً كما تفعل الـ Services في الـ Backend. الفرق هنا أنك لا تحتاج إلى مكتبة خارجية أو تعقيد إضافي، فقط React نفسه مع قليل من التفكير المعماري.
— تجربة عملية في مشاريع الإنتاج