اكتشف كيف تحول Custom Hooks في React منطق المكونات إلى وحدات مستقلة وقابلة لإعادة الاستخدام، مع أمثلة عملية من مشاريع حقيقية تكشف الأسرار خلف الأداء والذاكرة والمعالج.
في يوم من الأيام، وجدت نفسي أحدق في شاشة المحرر بينما الكود أمامي يتضخم بشكل مخيف. مكون واحد في تطبيق React كان يحتوي على أكثر من ٥٠٠ سطر، نصفها مكرر بين ١٢ مكون آخر. المشكلة لم تكن في الكود نفسه، بل في أن كل مكون كان يعيد اختراع العجلة: إدارة حالة الـ Form، استدعاء الـ API، التعامل مع الـ WebSocket، وحتى مراقبة حجم النافذة. هنا أدركت أن React لم تمنحنا فقط طريقة لكتابة واجهات المستخدم، بل أعطتنا أداة لتحويل الفوضى إلى نظام: Custom Hooks. لكن السؤال الحقيقي ليس كيف نكتبها، بل كيف نكتبها بطريقة تجعلها فعالة حقاً في بيئة الإنتاج.
الـ Custom Hooks ليست مجرد طريقة لتنظيم الكود، بل هي طبقة تجريدية تسمح لنا بفصل منطق العمل عن واجهة المستخدم. عندما أستخدم hook مثل useFetch في مشروع حقيقي، لا أريد فقط أن أحصل على البيانات، أريد أن أعرف متى يبدأ الطلب، متى ينتهي، ماذا يحدث إذا فشل، وكيف أتعامل مع الـ Race Conditions إذا أرسل المستخدم طلبين متتاليين بسرعة. هذه التفاصيل الصغيرة هي ما يميز الكود الذي يعمل في بيئة التطوير عن الكود الذي يبقى مستقراً في الإنتاج تحت ضغط آلاف المستخدمين المتزامنين.
الكثير من المطورين يعتقدون أن الـ Custom Hooks هي مجرد دوال عادية نُخرجها من المكون. لكن الحقيقة أعمق من ذلك بكثير. عندما تستخرج منطق إلى دالة عادية، تفقد القدرة على التفاعل مع دورة حياة المكون، ولا تستطيع استخدام الـ Hooks المدمجة مثل useState أو useEffect. أما الـ Custom Hook، فهو دالة يمكنها أن تحتفظ بحالتها الخاصة، وتستجيب للتغييرات في الـ Props، وتنفذ تأثيرات جانبية، وكل ذلك بطريقة متكاملة مع نظام React.
خذ مثلاً مكوناً يحتاج إلى مراقبة حجم نافذة المتصفح. إذا استخدمت دالة عادية، ستضطر إلى تحديث الحالة يدوياً في كل مرة يتغير الحجم، وهذا يعني أنك ستضطر إلى إدارة الحدث بنفسك وإلغاء الاشتراك عند إلغاء المكون. أما مع Custom Hook مثل useWindowSize، فستحصل على قيمة محدثة تلقائياً، وسيتعامل الـ Hook مع تنظيف الحدث عند إلغاء المكون. هذه ليست مجرد راحة، بل هي ضمان أن الكود لن يتسبب في تسريبات ذاكرة أو أحداث متكررة تستهلك موارد المعالج بلا داع.
// Custom Hook لقياس حجم النافذة
import { useState, useEffect } from 'react';
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
// تنظيف الحدث عند إلغاء المكون
return () => window.removeEventListener('resize', handleResize);
}, []); // مصفوفة الاعتمادات فارغة = تشغيل مرة واحدة
return size;
}
// استخدام في المكون
function ResponsiveComponent() {
const { width } = useWindowSize();
return <div>عرض النافذة: {width} بكسل</div>;
}في مشروع تجاري لإدارة المهام، واجهنا مشكلة مع مكونات الـ Form التي كانت تتكرر في أكثر من ٢٠ مكان. كل form كان يحتوي على منطق للتحقق من المدخلات، وإرسال البيانات إلى الـ API، وإدارة حالة الـ Loading، وعرض الأخطاء. بدلاً من تكرار الكود، أنشأنا Custom Hook اسمه useFormHandler يأخذ الـ Schema من Yup ويعيد دالة handleSubmit جاهزة للاستخدام. النتيجة؟ قلصنا الكود المكرر بنسبة ٨٠٪، وأصبح من السهل جداً تعديل منطق الـ Form في مكان واحد بدلاً من البحث في عشرات الملفات.
// Custom Hook لإدارة الـ Form مع Yup
import { useState } from 'react';
import * as yup from 'yup';
function useFormHandler(initialValues, schema) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const validate = async () => {
try {
await schema.validate(values, { abortEarly: false });
setErrors({});
return true;
} catch (err) {
const newErrors = {};
err.inner.forEach((error) => {
newErrors[error.path] = error.message;
});
setErrors(newErrors);
return false;
}
};
const handleChange = (e) => {
const { name, value } = e.target;
setValues({
...values,
[name]: value,
});
};
const handleSubmit = (onSubmit) => async (e) => {
e.preventDefault();
setIsSubmitting(true);
const isValid = await validate();
if (isValid) {
await onSubmit(values);
}
setIsSubmitting(false);
};
return {
values,
errors,
isSubmitting,
handleChange,
handleSubmit,
};
}
// مثال على الاستخدام
const schema = yup.object().shape({
email: yup.string().email().required(),
password: yup.string().min(8).required(),
});
function LoginForm() {
const { values, errors, isSubmitting, handleChange, handleSubmit } =
useFormHandler({ email: '', password: '' }, schema);
const async (values) => {
await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(values),
});
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
name="email"
value={values.email}
onChange={handleChange}
/>
{errors.email && <div>{errors.email}</div>}
<input
name="password"
type="password"
value={values.password}
onChange={handleChange}
/>
{errors.password && <div>{errors.password}</div>}
<button disabled={isSubmitting}>
{isSubmitting ? 'جاري الإرسال...' : 'تسجيل الدخول'}
</button>
</form>
);
}في مشروع آخر، كنا نعمل على لوحة تحكم تحليلية تعرض بيانات في الوقت الفعلي من خلال WebSocket. المشكلة كانت في أن كل مكون يحتاج إلى الاتصال بنفس الـ WebSocket وإدارة حالة الاتصال وإعادة الاتصال في حالة الفشل. بدلاً من تكرار هذا المنطق، أنشأنا Custom Hook اسمه useWebSocket يأخذ عنوان الـ WebSocket ويعيد حالة الاتصال، البيانات المستلمة، ودالة لإرسال الرسائل. هذا الـ Hook لم يقلل الكود المكرر فحسب، بل جعل من السهل جداً إضافة ميزات جديدة مثل إعادة الاتصال التلقائي أو تخزين الرسائل مؤقتاً في حالة انقطاع الاتصال.
// Custom Hook لإدارة اتصال WebSocket
import { useState, useEffect, useRef } from 'react';
function useWebSocket(url) {
const [isConnected, setIsConnected] = useState(false);
const [messages, setMessages] = useState([]);
const socketRef = useRef(null);
useEffect(() => {
const socket = new WebSocket(url);
socketRef.current = socket;
socket. () => {
setIsConnected(true);
};
socket.onmessage = (event) => {
setMessages((prev) => [...prev, event.data]);
};
socket.onclose = () => {
setIsConnected(false);
};
// إعادة الاتصال التلقائي
const reconnectInterval = setInterval(() => {
if (!isConnected && socket.readyState === WebSocket.CLOSED) {
const newSocket = new WebSocket(url);
socketRef.current = newSocket;
}
}, 5000);
return () => {
clearInterval(reconnectInterval);
socket.close();
};
}, [url]);
const sendMessage = (message) => {
if (isConnected && socketRef.current) {
socketRef.current.send(message);
}
};
return { isConnected, messages, sendMessage };
}
// استخدام في المكون
function Dashboard() {
const { isConnected, messages } = useWebSocket('wss://api.example.com/realtime');
return (
<div>
<div>الحالة: {isConnected ? 'متصل' : 'غير متصل'}</div>
<ul>
{messages.map((msg, index) => (
<li key={index}>{msg}</li>
))}
</ul>
</div>
);
}الكثير من المقالات تتحدث عن فوائد الـ Custom Hooks، لكنها نادراً ما تذكر المشاكل الحقيقية التي ستواجهها في الإنتاج. مثلاً، إذا استخدمت useEffect داخل Custom Hook واعتمدت على قيمة من الـ Props، يجب أن تضع هذه القيمة في مصفوفة الاعتمادات. إذا لم تفعل ذلك، قد ينتهي بك الأمر مع تأثيرات جانبية تعمل بشكل غير متوقع أو حتى تتسبب في حلقات لا نهائية. هذه ليست مجرد نصيحة، بل هي درس تعلمناه بالطريقة الصعبة عندما تسبب أحد الـ Hooks في تحميل السيرفر بأكثر من ١٠٠٠ طلب في الدقيقة بسبب حلقة تحديث لا تنتهي.
مشكلة أخرى شائعة هي الـ Stale Closures. عندما تستخدم قيمة من الحالة داخل useEffect، قد تحتفظ الـ Closure بالقيمة القديمة إذا لم تضع الاعتمادات الصحيحة. مثلاً، إذا كان لديك Custom Hook لإرسال بيانات إلى الـ API ويعتمد على قيمة من الحالة، وقد نسيت وضع هذه القيمة في مصفوفة الاعتمادات، فقد ترسل البيانات القديمة بدلاً من الجديدة. الحل؟ استخدم useRef لتتبع القيم التي تتغير بشكل متكرر، أو ضع جميع الاعتمادات في المصفوفة حتى لو كانت تبدو غير ضرورية.
// مثال على مشكلة Stale Closures وكيفية حلها
import { useState, useEffect, useRef } from 'react';
function useApiRequest(url) {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const urlRef = useRef(url); // استخدام useRef لتجنب Stale Closure
useEffect(() => {
urlRef.current = url; // تحديث القيمة في كل تغيير
}, [url]);
const fetchData = async () => {
setIsLoading(true);
try {
const resp await fetch(urlRef.current); // استخدام القيمة الحالية
const result = await response.json();
setData(result);
} catch (error) {
console.error('Error:', error);
} finally {
setIsLoading(false);
}
};
return { data, isLoading, fetchData };
}
// استخدام في المكون
function DataFetcher({ url }) {
const { data, isLoading, fetchData } = useApiRequest(url);
return (
<div>
<button onClick={fetchData} disabled={isLoading}>
جلب البيانات
</button>
{isLoading && <div>جاري التحميل...</div>}
{data && <div>{JSON.stringify(data)}</div>}
</div>
);
}الـ Custom Hooks ليست دائماً الحل الأمثل. إذا كنت تستخدمها بطريقة خاطئة، قد تتسبب في إعادة تحميل المكونات بلا داع أو حتى في تسريبات ذاكرة. مثلاً، إذا كان لديك Custom Hook يستخدم useEffect ويعتمد على قيمة من الـ Props، وكل تغيير في هذه القيمة يتسبب في إعادة تشغيل الـ Effect، فقد ينتهي بك الأمر مع مكون يعيد التحميل باستمرار. الحل؟ استخدم useMemo داخل الـ Hook لتخزين القيم التي لا تحتاج إلى إعادة حسابها في كل مرة، أو استخدم useCallback للدوال التي تمرر كـ Props إلى مكونات أخرى.
مشكلة أخرى تتعلق بالأداء هي الـ Re-renders غير الضرورية. إذا كان الـ Custom Hook يعيد كائناً جديداً في كل مرة، حتى لو لم تتغير القيم داخله، فقد يتسبب ذلك في إعادة تحميل المكونات التي تستخدم هذا الكائن. مثلاً، إذا كان لديك hook يعيد كائناً يحتوي على دوال، وكل مرة يعيد كائناً جديداً، فهذا يعني أن المكونات التي تستخدم هذه الدوال ستعيد التحميل حتى لو لم تتغير الدوال نفسها. الحل؟ استخدم useMemo لتخزين الكائن، أو أعد هيكلة الـ Hook ليعيد القيم بشكل فردي بدلاً من كائن.
// Custom Hook محسن لتجنب إعادة التحميل غير الضرورية
import { useState, useEffect, useMemo, useCallback } from 'react';
function useOptimizedApiRequest(url) {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const fetchData = useCallback(async () => {
setIsLoading(true);
try {
const resp await fetch(url);
const result = await response.json();
setData(result);
} catch (error) {
console.error('Error:', error);
} finally {
setIsLoading(false);
}
}, [url]); // الاعتماد على url فقط
// استخدام useMemo لتجنب إعادة إنشاء الكائن في كل مرة
const result = useMemo(() => ({
data,
isLoading,
fetchData,
}), [data, isLoading, fetchData]);
return result;
}
// استخدام في المكون
function OptimizedDataFetcher({ url }) {
const { data, isLoading, fetchData } = useOptimizedApiRequest(url);
return (
<div>
<button onClick={fetchData} disabled={isLoading}>
جلب البيانات
</button>
{isLoading && <div>جاري التحميل...</div>}
{data && <div>{JSON.stringify(data)}</div>}
</div>
);
}إذا كنت تريد كتابة Custom Hooks فعالة حقاً، فابدأ دائماً بالسؤال: "ماذا أريد أن يخفي هذا الـ Hook؟" الهدف ليس مجرد تقليل الكود المكرر، بل هو إنشاء طبقة تجريدية تخفي التعقيد وتوفر واجهة بسيطة للاستخدام. مثلاً، بدلاً من أن تجعل المستخدم يدير حالة الـ Loading والـ Error بنفسه، اجعل الـ Hook يعيد كائناً يحتوي على كل ما يحتاجه: البيانات، حالة التحميل، الأخطاء، والدوال اللازمة. بهذه الطريقة، يصبح الـ Hook وحدة مستقلة يمكن استخدامها في أي مكان دون الحاجة إلى معرفة تفاصيل تنفيذه الداخلية.
وأخيراً، تذكر أن الـ Custom Hooks ليست مجرد أداة لتنظيم الكود، بل هي طريقة للتفكير في منطق التطبيق. عندما تكتب hook جيداً، فأنت لا تكتب كوداً فقط، بل تخلق لغة جديدة للتفاعل مع المشكلة التي تحلها. لذا، في المرة القادمة التي تجد نفسك تكرر منطق معين، لا تفكر في كيفية استخراجه فقط، بل فكر في كيفية جعله سهل الاستخدام وقوي الأداء ومقاوم للأخطاء في بيئة الإنتاج الحقيقية.