اكتشف كيف تحول Custom Hooks في React من منطق متكرر إلى وحدات ذكية قابلة لإعادة الاستخدام، مع أمثلة حقيقية من مشاريع الإنتاج وتجنب الفخاخ الشائعة التي تكلف الفرق ساعات من تصحيح الأخطاء.
في مشروع إنتاجي قبل عامين، كان لدينا مكون واحد فقط مسؤول عن إدارة بيانات المستخدم، التحقق من الصلاحيات، وتحميل الملفات الشخصية. المكون كان يتضخم بسرعة مخيفة، حتى وصل إلى ٨٠٠ سطر من الكود، نصفها منطق متكرر يُستخدم في مكونات أخرى. المشكلة لم تكن فقط في الحجم، بل في الأداء: كل مرة نعدل فيها منطق التحقق من الصلاحيات، كنا ننسخ التعديل يدوياً في خمسة مكونات مختلفة، ومع كل نسخة جديدة، كان يظهر bug جديد. هنا دخلت Custom Hooks الصورة، ليس كحل نظري، بل كضرورة عملية أنقذت المشروع من الفشل.
الـ Custom Hooks ليست مجرد ميزة جميلة في React، بل هي أداة قوية لإعادة هيكلة الكود بطريقة تجعل منطق التطبيق قابلاً لإعادة الاستخدام دون تكرار أو تعقيد. الفكرة بسيطة: بدلاً من كتابة نفس الكود في كل مكون، نلف المنطق داخل دالة مخصصة تبدأ بكلمة use، ونعيد استخدام هذه الدالة في أي مكان نحتاج إليه. لكن خلف هذه البساطة تكمن تفاصيل تقنية مهمة: كيف يتعامل React مع حالة الـ Hooks داخلياً؟ لماذا يجب أن نتبع قواعد معينة عند كتابتها؟ وما هي الفخاخ التي يقع فيها حتى المطورون المحترفون؟
تخيل أنك تعمل على تطبيق إدارة مهام، وتحتاج في كل مكون إلى التحقق مما إذا كان المستخدم مسجلاً دخول أم لا. الحل التقليدي هو كتابة نفس الكود في كل مكون: استدعاء useState للحصول على حالة المستخدم، useEffect للاستماع لتغيرات الـ localStorage، وربما حتى استدعاء API للتحقق من صلاحية الـ token. النتيجة؟ نفس الكود يتكرر في ١٠ مكونات مختلفة، وكل تعديل صغير يتطلب تغييره في كل مكان. هذا ليس مجرد إهدار للوقت، بل هو كارثة للصيانة: إذا نسيت تعديل الكود في مكون واحد، ستجد نفسك أمام bug غريب يصعب تتبعه.
الـ Custom Hooks تحل هذه المشكلة بطريقة ذكية: بدلاً من تكرار الكود، نلف المنطق داخل دالة مخصصة، ونعيد استخدام هذه الدالة في أي مكون نحتاج إليه. لكن الأهم من ذلك هو كيف يتعامل React مع هذه الدوال داخلياً. عندما تستدعي useState داخل Custom Hook، React لا يعيد إنشاء الحالة من الصفر في كل مرة، بل يحتفظ بها كجزء من دورة حياة المكون. هذا يعني أن الحالة تبقى محفوظة بين عمليات إعادة الـ render، تماماً كما لو كنت تستخدم useState مباشرة داخل المكون. الفرق الوحيد هو أنك الآن تستطيع إعادة استخدام هذا المنطق في أي مكان دون تكرار الكود.
// مثال تقليدي بدون Custom Hook
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUser = async () => {
const resp await fetch('/api/user');
const data = await response.json();
setUser(data);
setLoading(false);
};
fetchUser();
}, []);
if (loading) return <div>جاري التحميل...</div>;
return <div>{user.name}</div>;
}
// نفس المنطق باستخدام Custom Hook
function useUser() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUser = async () => {
const response = await fetch('/api/user');
const data = await response.json();
setUser(data);
setLoading(false);
};
fetchUser();
}, []);
return { user, loading };
}
function UserProfile() {
const { user, loading } = useUser();
if (loading) return <div>جاري التحميل...</div>;
return <div>{user.name}</div>;
}عندما تستدعي Custom Hook داخل مكون، React لا يعيد إنشاء الحالة أو الـ Effects من الصفر في كل مرة. بدلاً من ذلك، يستخدم ما يسمى بـ Fiber Architecture لتتبع حالة كل hook داخل المكون. هذا يعني أن كل استدعاء لـ useState أو useEffect داخل Custom Hook يتم تسجيله كجزء من شجرة المكون، ويتم الاحتفاظ به بين عمليات إعادة الـ render. لكن هناك تفصيل مهم: إذا استخدمت نفس Custom Hook مرتين داخل نفس المكون، React يعامل كل استدعاء على أنه حالة مستقلة تماماً.
لفهم هذا بشكل أفضل، تخيل أنك تستخدم Custom Hook اسمه useCounter مرتين داخل نفس المكون. في المرة الأولى، تبدأ العداد بقيمة ٠، وفي المرة الثانية تبدأ بقيمة ١٠. React يحتفظ بكل حالة على حدة، ولا يحدث تداخل بينهما. هذا السلوك مشابه لكيفية تعامل React مع useState داخل المكون: إذا استخدمت useState مرتين، ستحصل على حالتين مستقلتين. الفرق الوحيد هو أن Custom Hook يسمح لك بتجميع منطق معقد داخل دالة واحدة وإعادة استخدامه بسهولة.
// مثال يوضح استقلال حالات Custom Hook
function useCounter(initialValue) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(c => c + 1);
return { count, increment };
}
function CounterComponent() {
const counter1 = useCounter(0); // حالة مستقلة
const counter2 = useCounter(10); // حالة مستقلة أخرى
return (
<div>
<button {counter1.increment}>زيادة العداد الأول: {counter1.count}</button>
<button onClick={counter2.increment}>زيادة العداد الثاني: {counter2.count}</button>
</div>
);
}أحد أكثر الأخطاء شيوعاً عند استخدام Custom Hooks هو نسيان أن الـ useEffect داخلها يعمل بنفس القواعد التي يعمل بها داخل المكون. هذا يعني أن الـ Effects لا تعمل بشكل متزامن، بل يتم إضافتها إلى الـ Event Loop وتنفذ بعد أن ينتهي الـ render الحالي. إذا كتبت Custom Hook يعتمد على تنفيذ effect بشكل متزامن، ستجد نفسك أمام سلوك غير متوقع، خاصة إذا كان الـ effect يعتمد على حالة أخرى داخل نفس الـ Hook.
على سبيل المثال، إذا كتبت Custom Hook يتحقق من حالة المستخدم ثم يقوم بتحميل بياناته، يجب أن تتأكد من أن الـ effect الثاني لا يعتمد على نتيجة الـ effect الأول بشكل متزامن. الحل هو إما تقسيم الـ effects إلى مراحل، أو استخدام useEffect مع dependencies صحيحة لضمان التنفيذ بالترتيب المطلوب. هذا التفصيل مهم جداً في التطبيقات الحقيقية، حيث قد يؤدي تجاهله إلى حالات سباق race conditions صعبة التتبع.
// مثال على Custom Hook مع effects متتالية
function useUserData(userId) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
useEffect(() => {
if (!userId) return;
const fetchUser = async () => {
const resp await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
};
fetchUser();
}, [userId]);
useEffect(() => {
if (!user) return;
const fetchPosts = async () => {
const response = await fetch(`/api/users/${user.id}/posts`);
const data = await response.json();
setPosts(data);
};
fetchPosts();
}, [user]); // يعتمد على user من الـ effect السابق
return { user, posts };
}في مشروع تجاري لإدارة المحتوى، كنا نستخدم نفس منطق التحقق من الصلاحيات في أكثر من ٣٠ مكون مختلف. المشكلة لم تكن فقط في التكرار، بل في الأداء: كل مكون كان يستدعي API للتحقق من الصلاحيات بشكل مستقل، مما يؤدي إلى تحميل زائد على السيرفر وإبطاء التطبيق. الحل كان كتابة Custom Hook اسمه useAuth يقوم بالتحقق مرة واحدة ويحفظ النتيجة في الـ localStorage، بحيث لا يحتاج أي مكون آخر إلى استدعاء API مرة أخرى طالما النتيجة مخزنة.
الـ Hook كان بسيطاً في المظهر، لكنه حوى تفاصيل مهمة: أولاً، كان يستخدم useEffect مع dependency على الـ token فقط، لضمان عدم إعادة التحقق إلا عند تغيير الـ token. ثانياً، كان يستخدم memoization لتجنب إعادة حساب القيم المحسوبة في كل render. ثالثاً، كان يتضمن منطق للتعامل مع الأخطاء، مثل إعادة محاولة الاتصال بالسيرفر في حالة الفشل. هذه التفاصيل الصغيرة هي ما جعلت الـ Hook فعالاً في بيئة الإنتاج.
// Custom Hook لإدارة الصلاحيات في مشروع حقيقي
function useAuth() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const checkAuth = async () => {
try {
const token = localStorage.getItem('token');
if (!token) {
setUser(null);
setLoading(false);
return;
}
const resp await fetch('/api/auth/verify', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) {
throw new Error('فشل التحقق من الصلاحيات');
}
const data = await response.json();
setUser(data.user);
} catch (err) {
setError(err.message);
setUser(null);
} finally {
setLoading(false);
}
};
checkAuth();
}, []); // يعتمد فقط على تغيير الـ token (غير مباشر)
const login = async (credentials) => {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
if (!response.ok) {
throw new Error('فشل تسجيل الدخول');
}
const { token, user } = await response.json();
localStorage.setItem('token', token);
setUser(user);
} catch (err) {
setError(err.message);
throw err;
}
};
const logout = () => {
localStorage.removeItem('token');
setUser(null);
};
return { user, loading, error, login, logout };
}في مشروع آخر، كنا نحتاج إلى إدارة نماذج معقدة تحتوي على حقول متداخلة، وتحقق من الصحة في الوقت الفعلي، وإرسال البيانات إلى API. بدلاً من استخدام مكتبة خارجية مثل Formik أو React Hook Form، قررنا كتابة Custom Hook مخصص لإدارة النماذج. الـ Hook كان مسؤولاً عن تتبع حالة كل حقل، التحقق من الصحة، وإرسال البيانات عند الضغط على زر الإرسال. الميزة الرئيسية لهذا النهج هي أننا حصلنا على تحكم كامل في منطق النموذج دون الاعتماد على مكتبات خارجية قد تضيف تعقيداً غير ضروري.
الـ Hook كان يستخدم useReducer لإدارة الحالة المعقدة بدلاً من useState، مما سمح لنا بتجميع كل المنطق في مكان واحد وتجنب التحديثات غير الضرورية. كما كان يتضمن منطق للتحقق من الصحة يعتمد على قواعد محددة لكل حقل، ويعيد رسائل خطأ مخصصة. هذا النهج جعل المكونات التي تستخدم الـ Hook نظيفة للغاية، حيث لم تكن بحاجة إلى معرفة أي شيء عن منطق التحقق أو الإرسال.
// Custom Hook لإدارة النماذج المعقدة
function useForm(initialValues, validate) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setValues({
...values,
[name]: value
});
};
const handleSubmit = async (onSubmit) => {
setIsSubmitting(true);
const validati validate(values);
setErrors(validationErrors);
if (Object.keys(validationErrors).length === 0) {
try {
await onSubmit(values);
} catch (err) {
console.error('فشل الإرسال:', err);
}
}
setIsSubmitting(false);
};
return {
values,
errors,
isSubmitting,
handleChange,
handleSubmit
};
}
// استخدام الـ Hook في مكون
function LoginForm() {
const validate = (values) => {
const errors = {};
if (!values.email) errors.email = 'البريد الإلكتروني مطلوب';
if (!values.password) errors.password = 'كلمة المرور مطلوبة';
return errors;
};
const { values, errors, isSubmitting, handleChange, handleSubmit } = useForm({
email: '',
password: ''
}, validate);
const onSubmit = async (values) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values)
});
const data = await response.json();
console.log('تم تسجيل الدخول:', data);
};
return (
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(onSubmit); }}>
<input
type="email"
name="email"
value={values.email}
onChange={handleChange}
/>
{errors.email && <div>{errors.email}</div>}
<input
type="password"
name="password"
value={values.password}
onChange={handleChange}
/>
{errors.password && <div>{errors.password}</div>}
<button type="submit" disabled={isSubmitting}>
تسجيل الدخول
</button>
</form>
);
}أول فخ يقع فيه المطورون هو تجاهل قواعد الـ Hooks الأساسية. على سبيل المثال، استدعاء Custom Hook داخل شرط أو حلقة سيؤدي إلى خطأ في React، لأن React يعتمد على ترتيب استدعاء الـ Hooks لتتبع الحالة. هذا يعني أنك لا تستطيع كتابة شيء مثل if (condition) { useCustomHook(); }، لأن React سيفقد تتبع الحالة وسيظهر خطأ غامض.
الفخ الثاني هو نسيان أن الـ Custom Hooks ليست مكونات، وبالتالي لا يمكنها استخدام الـ JSX مباشرة. إذا حاولت إعادة عنصر React من Custom Hook، ستحصل على خطأ. الحل هو إعادة البيانات فقط، وترك الـ JSX للمكون الذي يستخدم الـ Hook. هذا التفصيل مهم جداً، خاصة للمطورين الذين ينتقلون من كتابة المكونات إلى كتابة الـ Hooks.
أحد أكثر المشاكل خطورة عند استخدام Custom Hooks هو تسريب الذاكرة، خاصة إذا كان الـ Hook يستخدم عمليات غير متزامنة مثل استدعاءات API أو الاشتراكات في الأحداث. إذا لم تقم بتنظيف الـ effects بشكل صحيح، قد تجد نفسك أمام مشكلة حيث يستمر الـ effect في العمل حتى بعد إزالة المكون من الـ DOM، مما يؤدي إلى أخطاء أو تسريب ذاكرة. الحل هو دائماً إضافة cleanup function داخل useEffect لضمان إيقاف أي عمليات غير متزامنة عند إزالة المكون.
على سبيل المثال، إذا كان الـ Custom Hook يستمع لتغيرات في قاعدة بيانات Firebase، يجب عليك إلغاء الاشتراك عند إزالة المكون. إذا نسيت القيام بذلك، ستجد أن الـ effect يستمر في العمل حتى بعد إغلاق الصفحة، مما يؤدي إلى أخطاء في وحدة التحكم وربما تسريب ذاكرة. هذا التفصيل مهم جداً في التطبيقات الكبيرة، حيث قد يؤدي تجاهله إلى مشاكل أداء يصعب تتبعها.
// Custom Hook مع cleanup لتجنب تسريب الذاكرة
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); // cleanup
};
}, []); // لا يعتمد على أي dependencies
return size;
}إذا كنت تريد كتابة Custom Hooks فعالة، ابدأ دائماً بتحديد المشكلة التي تريد حلها بدقة. لا تكتب Hook لمجرد أنه يبدو جميلاً، بل اكتبه عندما تجد نفسك تكرر نفس المنطق في ثلاثة مكونات أو أكثر. ثانياً، تذكر دائماً أن الـ Custom Hooks ليست مكونات، بل هي وحدات منطق قابلة لإعادة الاستخدام، لذلك لا تحاول إرجاع JSX منها. ثالثاً، اهتم بالتفاصيل الصغيرة مثل cleanup في useEffect وmemoization للقيم المحسوبة، لأنها ما يميز الـ Hook الجيد عن الـ Hook الذي يسبب مشاكل في الإنتاج.
وأخيراً، لا تخف من تجربة أشياء جديدة. الـ Custom Hooks تمنحك حرية كبيرة في هيكلة الكود، فاستغل هذه الحرية لكتابة منطق نظيف وفعال. وإذا واجهت مشكلة، تذكر أن React يوفر أدوات قوية لتصحيح الأخطاء مثل React DevTools، استخدمها لتتبع حالة الـ Hooks وفهم ما يحدث خلف الكواليس. في النهاية، الـ Custom Hooks ليست مجرد ميزة في React، بل هي طريقة تفكير جديدة في هيكلة التطبيقات، فاستخدمها بحكمة.