اكتشف كيف تحول Custom Hooks في React من منطق متكرر إلى وحدات ذكية قابلة لإعادة الاستخدام، مع أمثلة حقيقية من مشاريع الإنتاج وكيفية تجنب الفخاخ التي تدمر الأداء.
في مشروعنا الأخير مع عميل في مجال التجارة الإلكترونية، كان لدينا مكون React واحد فقط يعالج الدفع عبر بوابة Stripe. بعد ثلاثة أشهر، أصبح هذا المكون يتكرر في خمسة أماكن مختلفة: صفحة المنتج، عربة التسوق، الصفحة الشخصية، وحتى في صفحة الهبوط الجديدة. المشكلة؟ كل نسخة كانت تحتوي على نفس الكود بالضبط: التحقق من الاتصال، إدارة حالة التحميل، معالجة الأخطاء، وتحديث السياق العالمي. عندما قرّرنا تغيير طريقة معالجة الأخطاء، اضطررنا لتعديل نفس الكود في خمسة أماكن. هذا ليس مجرد تكرار مزعج، بل هو كابوس للصيانة وكارثة للأداء. هنا تأتي Custom Hooks لإنقاذنا، لكنها ليست مجرد حل سحري—هي أداة هندسية تحتاج فهمًا عميقًا لكيفية عمل React تحت الغطاء.
الـ Custom Hook في React ليس مجرد طريقة لتنظيم الكود، بل هو نمط تصميم يسمح لك باستخراج منطق الحالة والسلوك من المكونات إلى وحدات مستقلة يمكن إعادة استخدامها. لكن لماذا هذا مهم؟ لأن React نفسها تستخدم نفس المبدأ داخليًا. عندما تستخدم useState أو useEffect، فأنت بالفعل تستخدم hooks مدمجة تم تصميمها لتجنب تكرار منطق الحالة. المشكلة الحقيقية تبدأ عندما تجد نفسك تكتب نفس الكود في مكونات متعددة، وهذا يحدث عادةً عندما يكون لديك منطق معقد يعتمد على الحالة أو التأثيرات الجانبية. مثلاً، منطق جلب البيانات من API مع إدارة حالة التحميل والأخطاء هو مثال كلاسيكي. بدلاً من تكرار هذا المنطق في كل مكون، يمكنك استخراجه إلى custom hook واحد، مثل useFetch، واستخدامه في أي مكان تحتاج إليه.
دعونا نتحدث بالأرقام. في مشروع متوسط الحجم، وجدنا أن تكرار منطق جلب البيانات في 10 مكونات مختلفة أدى إلى زيادة حجم ملف الجافا سكريبت النهائي بمقدار 32 كيلوبايت بعد التجميع. قد يبدو هذا الرقم صغيرًا، لكن في بيئة متصفح ضعيفة أو على شبكات بطيئة، هذه الـ 32 كيلوبايت تعني تأخيرًا إضافيًا في تحميل الصفحة بمقدار 200 إلى 500 مللي ثانية. والأمر الأسوأ هو أن كل مرة تقوم فيها بتعديل هذا المنطق المتكرر، فأنت تزيد من فرصة إدخال أخطاء جديدة. في إحدى المرات، نسينا تحديث أحد المكونات بعد تغيير في منطق معالجة الأخطاء، مما أدى إلى سلوك غير متوقع في واجهة المستخدم استمر لأيام قبل اكتشافه. هذا النوع من الأخطاء ليس فقط مكلفًا من حيث الوقت، بل يدمر ثقة الفريق في الكود.
لكن المشكلة الحقيقية ليست فقط في حجم الكود أو الأخطاء البشرية. عندما تكرر منطق الحالة في مكونات متعددة، فإنك تخلق حالة من عدم الاتساق في كيفية إدارة React لهذه الحالة تحت الغطاء. مثلاً، إذا كان لديك useEffect في مكونين مختلفين يقومان بنفس الشيء، فستجد أن React قد يقوم بإعادة تشغيل هذه التأثيرات في أوقات مختلفة بناءً على ترتيب تحديث المكونات في شجرة الـ Virtual DOM. هذا يمكن أن يؤدي إلى سلوك غير متوقع، خاصة عندما تعتمد هذه التأثيرات على بعضها البعض. Custom Hooks تحل هذه المشكلة لأنها تضمن أن منطق الحالة يتم تنفيذه بنفس الطريقة في كل مرة، بغض النظر عن مكان استخدامه.
لنبدأ بمثال عملي من مشروع حقيقي. لدينا مكون يعرض قائمة المنتجات من API، مع إمكانية التصفية والبحث. الكود الأصلي كان يحتوي على useState لإدارة حالة التحميل، useEffect لجلب البيانات، وuseState آخر لإدارة حالة الخطأ. عندما قررنا إضافة ميزة التحديث التلقائي كل 30 ثانية، وجدنا أنفسنا نكرر نفس الكود في ثلاثة مكونات مختلفة. الحل؟ استخراج هذا المنطق إلى custom hook اسمه useProducts.
// useProducts.js
import { useState, useEffect } from 'react';
const useProducts = (initialUrl, opti {}) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [url, setUrl] = useState(initialUrl);
const { autoRefresh = false, refreshInterval = 30000 } = options;
useEffect(() => {
let isMounted = true;
let intervalId = null;
const fetchData = async () => {
setLoading(true);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (isMounted) {
setData(result);
setError(null);
}
} catch (err) {
if (isMounted) {
setError(err.message);
setData(null);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
fetchData();
if (autoRefresh) {
intervalId = setInterval(fetchData, refreshInterval);
}
return () => {
isMounted = false;
if (intervalId) {
clearInterval(intervalId);
}
};
}, [url, autoRefresh, refreshInterval]);
const refetch = (newUrl = url) => {
setUrl(newUrl);
};
return { data, loading, error, refetch };
};
export default useProducts;هذا الكود ليس مجرد استخراج للمتغيرات. لاحظ كيف استخدمنا isMounted لمنع تحديث الحالة بعد إلغاء تحميل المكون، وهي مشكلة شائعة تؤدي إلى أخطاء مثل "Can't perform a React state update on an unmounted component". أيضًا، قمنا بإدارة الـ interval بشكل صحيح في cleanup function لمنع تسرب الذاكرة. هذا النوع من التفاصيل هو ما يميز custom hook جيد من واحد سيء. عندما تستخدم هذا الـ hook في مكون، يصبح الكود نظيفًا جدًا:
// ProductList.js
import React from 'react';
import useProducts from './useProducts';
const ProductList = () => {
const { data: products, loading, error, refetch } = useProducts(
'https://api.example.com/products',
{ autoRefresh: true }
);
if (loading) return <div>جاري التحميل...</div>;
if (error) return <div>خطأ: {error}</div>;
return (
<div>
<button {() => refetch('https://api.example.com/products?sort=price')}>
ترتيب حسب السعر
</button>
<ul>
{products.map(product => (
<li key={product.id}>{product.name} - {product.price}</li>
))}
</ul>
</div>
);
};
export default ProductList;الآن، إذا أردنا تغيير منطق جلب البيانات—مثل إضافة headers للمصادقة أو تغيير طريقة معالجة الأخطاء—فنحن نقوم بتعديل ملف واحد فقط: useProducts.js. هذا النوع من التنظيم ليس مجرد تحسين للكود، بل هو تغيير في طريقة تفكيرنا كمطورين. بدلاً من التفكير في المكونات كوحدات تحتوي على منطق، نبدأ في التفكير في منطق الحالة والسلوك كوحدات مستقلة يمكن إعادة استخدامها.
عندما تستخدم custom hook مثل useProducts، فإن React لا تقوم بإنشاء نسخة جديدة من الكود في كل مرة. بدلاً من ذلك، يتم تنفيذ الكود داخل سياق المكون الذي يستخدم الـ hook، مما يعني أن كل مكون يحصل على نسخة مستقلة من حالة الـ hook. هذا مهم لفهمه لأن الكثير من المطورين يعتقدون خطأً أن custom hooks تعمل مثل المكتبات العادية التي تحتفظ بحالة عامة. في الواقع، كل استدعاء لـ useProducts في مكون مختلف ينشئ إغلاقًا جديدًا (closure) مع حالته الخاصة.
لنأخذ مثالًا على ذلك. إذا كان لديك مكونان يستخدمان useProducts بنفس الـ URL، فسيكون لكل منهما حالته الخاصة من data، loading، وerror. هذا يعني أنه إذا قام أحد المكونات بتحديث حالته، فلن يؤثر ذلك على المكون الآخر. هذا السلوك مشابه لكيفية عمل useState في المكونات العادية. لكن هناك فخ هنا: إذا قمت بتمرير نفس الكائن أو المصفوفة إلى custom hook عبر props، فقد ينتهي بك الأمر بمشاركة الحالة بطريق الخطأ بسبب كيفية عمل مراجع JavaScript. على سبيل المثال:
// ❌ خطأ شائع: مشاركة الحالة بطريق الخطأ
const opti { autoRefresh: true };
const ComponentA = () => {
const { data } = useProducts('https://api.example.com/products', options);
// ...
};
const ComponentB = () => {
const { data } = useProducts('https://api.example.com/products', options);
// ...
};في هذا المثال، إذا قمت بتعديل options في مكان ما في الكود، فقد يؤثر ذلك على كلا المكونين لأنهما يشتركان في نفس مرجع الكائن. الحل؟ إما أن تمرر قيمًا أولية بسيطة (مثل الأعداد أو السلاسل النصية) أو تنشئ كائنًا جديدًا داخل المكون:
// ✅ الحل الصحيح
const Comp () => {
const { data } = useProducts('https://api.example.com/products', { autoRefresh: true });
// ...
};ليس كل منطق يحتاج إلى custom hook. في الواقع، الإفراط في استخدامهم يمكن أن يجعل الكود أكثر تعقيدًا دون داع. القاعدة الأساسية التي أتبعها هي: إذا وجدت نفسك تكتب نفس الكود في ثلاثة مكونات مختلفة أو أكثر، فهذا مؤشر جيد لاستخراجه إلى custom hook. لكن هناك حالات أخرى حيث يكون استخدامهم مفيدًا حتى لو لم يكن هناك تكرار واضح:
من ناحية أخرى، هناك حالات يجب فيها تجنب استخدام Custom Hooks:
في أحد المشاريع مع شركة ناشئة في مجال الصحة الرقمية، كان لدينا نموذج تسجيل معقد يحتوي على 15 حقلاً مختلفًا، كل منها له قواعد تحقق خاصة. بدلاً من كتابة نفس الكود للتحقق من الصحة وإدارة الحالة في كل مكون، قمنا ببناء custom hook اسمه useForm الذي يتعامل مع كل شيء. هذا الـ hook ليس مجرد إدارة للحالة، بل هو نظام كامل للتحقق من الصحة وإرسال البيانات.
// useForm.js
import { useState } from 'react';
const 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 (callback) => {
setIsSubmitting(true);
const validati validate(values);
setErrors(validationErrors);
if (Object.keys(validationErrors).length === 0) {
try {
await callback(values);
} catch (err) {
setErrors({ submit: err.message });
}
}
setIsSubmitting(false);
};
return {
values,
errors,
isSubmitting,
handleChange,
handleSubmit,
};
};
export default useForm;الآن، يمكننا استخدام هذا الـ hook في أي نموذج بسهولة:
// RegistrationForm.js
import React from 'react';
import useForm from './useForm';
const validate = (values) => {
const errors = {};
if (!values.email) {
errors.email = 'البريد الإلكتروني مطلوب';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) {
errors.email = 'البريد الإلكتروني غير صالح';
}
if (!values.password) {
errors.password = 'كلمة المرور مطلوبة';
} else if (values.password.length < 8) {
errors.password = 'كلمة المرور يجب أن تكون 8 أحرف على الأقل';
}
return errors;
};
const Registrati () => {
const { values, errors, isSubmitting, handleChange, handleSubmit } = useForm(
{ email: '', password: '' },
validate
);
const onSubmit = async (values) => {
await fetch('https://api.example.com/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values),
});
};
return (
<form onSubmit={(e) => {
e.preventDefault();
handleSubmit(onSubmit);
}}>
<div>
<label>البريد الإلكتروني</label>
<input
type="email"
name="email"
value={values.email}
onChange={handleChange}
/>
{errors.email && <span>{errors.email}</span>}
</div>
<div>
<label>كلمة المرور</label>
<input
type="password"
name="password"
value={values.password}
onChange={handleChange}
/>
{errors.password && <span>{errors.password}</span>}
</div>
{errors.submit && <div>{errors.submit}</div>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'جاري التسجيل...' : 'تسجيل'}
</button>
</form>
);
};
export default RegistrationForm;هذا المثال يظهر قوة Custom Hooks في تبسيط منطق معقد. بدلاً من كتابة نفس الكود للتحقق من الصحة وإدارة الحالة في كل نموذج، لدينا الآن نظام قابل لإعادة الاستخدام يمكن تعديله بسهولة. إذا أردنا إضافة ميزة جديدة، مثل التحقق من الصحة في الوقت الفعلي أثناء الكتابة، فنحن نقوم بتعديل ملف واحد فقط: useForm.js.
عندما تبدأ في استخدام Custom Hooks بشكل مكثف، ستواجه بعض المشاكل التي قد لا تكون واضحة في البداية. واحدة من أكثر المشاكل شيوعًا هي ما يسمى بـ "stale closure". يحدث هذا عندما تحتفظ الـ closure داخل الـ hook بمرجع قديم لقيمة تغيرت لاحقًا. على سبيل المثال:
// ❌ مشكلة stale closure
const useCounter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1); // هنا count ستكون القيمة القديمة دائمًا
}, 1000);
return () => clearInterval(interval);
}, []); // ❌ الاعتماديات فارغة!
return count;
};في هذا المثال، الـ count داخل الـ setInterval ستكون دائمًا القيمة الأولية (0) لأن الـ closure تحتفظ بالقيمة القديمة. الحل هو استخدام النسخة الوظيفية من setCount أو إضافة الاعتماديات الصحيحة:
// ✅ الحل الصحيح
const useCounter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(prevCount => prevCount + 1); // استخدام النسخة الوظيفية
}, 1000);
return () => clearInterval(interval);
}, []); // الآن يمكننا ترك الاعتماديات فارغة
return count;
};مشكلة أخرى شائعة هي استخدام Custom Hooks داخل حلقات أو شروط. على سبيل المثال، لا يمكنك استدعاء useSomething داخل if أو for لأن React تعتمد على ترتيب استدعاء الـ hooks لتتبع الحالة الداخلية. إذا تغير عدد الـ hooks بين عمليات الـ render، فسيؤدي ذلك إلى سلوك غير متوقع. الحل هو دائمًا استدعاء الـ hooks في المستوى الأعلى من المكون أو الـ custom hook الخاص بك.
أيضًا، كن حذرًا عند استخدام Custom Hooks التي تعتمد على Context API. إذا قمت بإنشاء سياق جديد داخل الـ hook، فسيتم إنشاء سياق جديد في كل مرة يتم فيها استدعاء الـ hook، مما قد يؤدي إلى سلوك غير متوقع. بدلاً من ذلك، قم بإنشاء السياق خارج الـ hook واستخدمه داخله:
// ❌ خطأ: إنشاء سياق جديد في كل مرة
const useTheme = () => {
const ThemeC React.createContext('light');
const [theme, setTheme] = useState('light');
return { theme, setTheme };
};
// ✅ صحيح: إنشاء السياق خارج الـ hook
const ThemeContext = React.createContext('light');
const useTheme = () => {
const { theme, setTheme } = useContext(ThemeContext);
return { theme, setTheme };
};إذا أخذت شيئًا واحدًا فقط من هذا المقال، فليكن هذا: Custom Hooks ليست مجرد أداة لتنظيم الكود، بل هي نمط تفكير. عندما تبدأ في التفكير في منطق الحالة والسلوك كوحدات مستقلة يمكن إعادة استخدامها، ستبدأ في رؤية الفرص لاستخراج هذا المنطق في كل مكان. لكن تذكر، القوة تأتي مع المسؤولية. كل custom hook تضيفه إلى مشروعك يجب أن يكون له غرض واضح وقابل للاختبار وسهل الفهم. إذا وجدت نفسك تبني hook معقد يحتوي على 200 سطر من الكود، فربما حان الوقت لإعادة التفكير في التصميم. القاعدة التي أتبعها هي: إذا كان الـ hook يحتاج إلى أكثر من 50 سطر من الكود، فربما يجب تقسيمه إلى hooks أصغر وأكثر تركيزًا. بهذه الطريقة، ستحصل على كود قابل لإعادة الاستخدام دون التضحية بالبساطة أو الأداء.
الخطوة التالية؟ ابدأ بتحويل أحد المكونات المتكررة في مشروعك الحالي إلى custom hook. ابدأ بشيء بسيط، مثل منطق جلب البيانات أو إدارة حالة النموذج، ثم انتقل إلى منطق أكثر تعقيدًا. ستجد أن الكود يصبح أكثر نظافة، والصيانة تصبح أسهل، والفرق بين المكونات يصبح أكثر وضوحًا. وعندما ترى كيف يمكن إعادة استخدام نفس الـ hook في مكونات مختلفة دون تكرار، ستدرك أن Custom Hooks ليست مجرد ميزة في React، بل هي ثورة في كيفية بناء واجهات المستخدم الحديثة.