كيف تحول منطق المكونات المعقد إلى Custom Hooks ذكية؟ اكتشف أمثلة حقيقية من مشاريع الإنتاج، تجنب الفخاخ الشائعة، وافهم ما يحدث خلف الكواليس في الذاكرة والمعالج.
في أحد مشاريع الإنتاج الكبيرة لشركة ناشئة في مجال الصحة الرقمية، كان لدينا مكون واحد فقط مسؤول عن إدارة بيانات المريض، الحجوزات، والتذكيرات الدوائية. المكون أصبح يتضخم ليحتوي على أكثر من ٥٠٠ سطر من الكود، وكان يعيد تحميل البيانات كل مرة نغير فيها فلتر بسيط. المشكلة لم تكن في الأداء فقط، بل في أن ثلاثة فرق مختلفة كانت تعمل على نفس الملف، وكل تغيير صغير كان يتسبب في conflicts لا تنتهي. هنا جاءت فكرة Custom Hooks كحل سحري، لكنها لم تكن مجرد تقسيم للكود، بل إعادة تصميم كاملة لكيفية تعاملنا مع منطق العمل في React.
عندما نتحدث عن Custom Hooks، لا نتحدث عن مجرد دالة تعيد قيمتين. نتحدث عن نمط برمجي يسمح لنا باستخراج منطق العمل المعقد من المكونات، وإعادة استخدامه عبر التطبيق بأكمله دون تكرار الكود. الفرق بين Custom Hook جيد وسيء يمكن أن يكون الفرق بين تطبيق سريع الاستجابة وآخر يتجمد عند كل تفاعل. في هذا المقال، سنغوص في أمثلة حقيقية من مشاريع الإنتاج، ونشرح كيف تعمل Custom Hooks خلف الكواليس، وما هي الفخاخ التي يقع فيها حتى المطورون ذوو الخبرة.
الكثير من المطورين ينظرون إلى Custom Hooks على أنها مجرد دوال عادية يمكن استدعاؤها داخل المكونات. لكن الحقيقة أعمق من ذلك بكثير. عندما تستدعي Custom Hook داخل مكون React، فإن React يقوم بتتبع هذه الاستدعاءات بنفس الطريقة التي يتتبع بها استدعاءات useState أو useEffect. هذا يعني أن React يعرف متى يجب إعادة تنفيذ هذه الدالة، ومتى يمكنها تخطي التنفيذ بناءً على التغييرات في الـ dependencies. هذه الميزة ليست مجرد تحسين للأداء، بل هي ما يجعل Custom Hooks أداة قوية لإدارة الحالة والside effects بطريقة متوقعة.
في أحد المشاريع التي عملت عليها، كان لدينا Custom Hook اسمه useUserSession لإدارة جلسة المستخدم. هذا الـ Hook كان يتعامل مع تخزين الـ token في localStorage، وإدارة صلاحية الجلسة، وتحديث واجهة المستخدم عند انتهاء الجلسة. بدون فهم كيفية تعامل React مع هذه الاستدعاءات، كنا سنقع في مشكلة شائعة: إعادة تحميل البيانات بشكل غير متوقع عند تغيير حالة غير ذات صلة. الحل كان في فهم أن React يستخدم خوارزمية مقارنة تعتمد على ترتيب استدعاء الـ Hooks، وليس على محتواها. هذا يعني أنه إذا غيرنا ترتيب استدعاء useUserSession مع أي hook آخر، فإن React سيفقد القدرة على تتبع الحالة بشكل صحيح، مما يؤدي إلى سلوك غير متوقع.
// مثال على Custom Hook لإدارة جلسة المستخدم
import { useState, useEffect } from 'react';
const useUserSession = (initialToken = null) => {
const [token, setToken] = useState(initialToken);
const [isAuthenticated, setIsAuthenticated] = useState(!!initialToken);
const [sessionExpiry, setSessionExpiry] = useState(null);
// تحديث الجلسة عند تغيير الـ token
useEffect(() => {
if (token) {
const expiry = new Date();
expiry.setHours(expiry.getHours() + 1); // صلاحية الجلسة ساعة واحدة
setSessionExpiry(expiry);
localStorage.setItem('authToken', token);
setIsAuthenticated(true);
} else {
localStorage.removeItem('authToken');
setIsAuthenticated(false);
setSessionExpiry(null);
}
}, [token]);
// التحقق من انتهاء الجلسة كل دقيقة
useEffect(() => {
const interval = setInterval(() => {
if (sessionExpiry && new Date() > sessionExpiry) {
setToken(null); // انتهاء الجلسة
}
}, 60000);
return () => clearInterval(interval);
}, [sessionExpiry]);
const login = (newToken) => setToken(newToken);
const logout = () => setToken(null);
return { token, isAuthenticated, sessionExpiry, login, logout };
};
// استخدام الـ Hook داخل مكون
const UserProfile = () => {
const { isAuthenticated, logout } = useUserSession();
if (!isAuthenticated) {
return <div>يرجى تسجيل الدخول</div>;
}
return (
<div>
<h1>مرحباً بك!</h1>
<button {logout}>تسجيل الخروج</button>
</div>
);
};في مشروع لشركة تقدم خدمات تحليل البيانات المالية، كان لدينا مكون اسمه StockDashboard يعرض بيانات الأسهم في الوقت الفعلي. المشكلة كانت أن هذا المكون كان يحتوي على منطق معقد لإدارة الاشتراكات في WebSocket، وتحديث البيانات عند تلقي تحديثات جديدة، والتعامل مع الأخطاء عند انقطاع الاتصال. عند محاولة إعادة استخدام هذا المنطق في مكون آخر، وجدنا أنفسنا نكرر نفس الكود مع تعديلات طفيفة، مما أدى إلى مشاكل في الصيانة. الحل كان في استخراج هذا المنطق إلى Custom Hook اسمه useStockData.
هذا الـ Hook لم يكن مجرد استخراج للكود، بل كان إعادة تصميم كاملة لكيفية تعاملنا مع البيانات في الوقت الفعلي. بدلاً من أن يكون المكون مسؤولاً عن فتح وإغلاق اتصال WebSocket، أصبح الـ Hook هو المسؤول عن ذلك، ويوفر للمكون واجهة بسيطة لاستخدام البيانات. هذا التغيير لم يجعل الكود أكثر نظافة فحسب، بل حسن الأداء أيضاً، لأننا تمكنا من إضافة منطق ذكي لإعادة الاتصال تلقائياً عند انقطاع الاتصال، دون أن يؤثر ذلك على المكونات التي تستخدم الـ Hook.
// Custom Hook لإدارة بيانات الأسهم في الوقت الفعلي
import { useState, useEffect, useCallback } from 'react';
const useStockData = (symbols) => {
const [stockData, setStockData] = useState({});
const [isConnected, setIsConnected] = useState(false);
const [error, setError] = useState(null);
const [socket, setSocket] = useState(null);
// دالة لإعادة الاتصال بعد انقطاع
const rec useCallback(() => {
if (socket) {
socket.close();
}
const newSocket = new WebSocket(`wss://api.stockdata.com/ws?symbols=${symbols.join(',')}`);
newSocket.onopen = () => {
setIsConnected(true);
setError(null);
};
newSocket.onmessage = (event) => {
const data = JSON.parse(event.data);
setStockData(prev => ({ ...prev, [data.symbol]: data }));
};
newSocket.onerror = () => {
setError('فشل الاتصال بخادم البيانات');
};
newSocket.onclose = () => {
setIsConnected(false);
// محاولة إعادة الاتصال بعد 5 ثوان
setTimeout(reconnect, 5000);
};
setSocket(newSocket);
}, [symbols]);
useEffect(() => {
reconnect();
return () => {
if (socket) {
socket.close();
}
};
}, [reconnect]);
return { stockData, isConnected, error };
};
// استخدام الـ Hook داخل مكون
const StockDashboard = ({ symbols }) => {
const { stockData, isConnected, error } = useStockData(symbols);
if (error) {
return <div className="error">{error}</div>;
}
if (!isConnected) {
return <div className="loading">جاري الاتصال بالخادم...</div>;
}
return (
<div className="stock-dashboard">
{symbols.map(symbol => (
<div key={symbol} className="stock-card">
<h3>{symbol}</h3>
<p>السعر: {stockData[symbol]?.price || 'جاري التحميل'}</p>
<p>التغير: {stockData[symbol]?.change || '0'}%</p>
</div>
))}
</div>
);
};في أحد المشاريع التي عملت عليها، لاحظنا أن التطبيق يبدأ في التباطؤ بشكل ملحوظ بعد استخدامه لفترة طويلة. بعد التحقيق، اكتشفنا أن المشكلة كانت في Custom Hook اسمه useDocumentTitle الذي كنا نستخدمه لتحديث عنوان الصفحة بناءً على حالة التطبيق. المشكلة كانت أن هذا الـ Hook كان يضيف event listener عند تحميل المكون، لكنه لم يكن يزيله عند إلغاء تحميل المكون. هذا أدى إلى تراكم الـ event listeners في الذاكرة، مما تسبب في ما يعرف بـ Memory Leak.
الحل لم يكن مجرد إضافة cleanup function في useEffect، بل كان في فهم كيفية تعامل React مع الـ cleanup. عندما تقوم بإرجاع دالة من useEffect، فإن React يقوم بتنفيذ هذه الدالة عند إلغاء تحميل المكون، وأيضاً قبل تنفيذ الـ effect مرة أخرى إذا تغيرت الـ dependencies. هذا يعني أنه إذا كان لديك Custom Hook يستخدم useEffect، فيجب عليك دائماً التأكد من أن الـ cleanup function تزيل أي موارد تم تخصيصها، مثل event listeners، intervals، أو اشتراكات في WebSocket.
// Custom Hook لتحديث عنوان الصفحة مع تجنب Memory Leak
import { useEffect } from 'react';
const useDocumentTitle = (title) => {
useEffect(() => {
const originalTitle = document.title;
document.title = title;
// cleanup function لإرجاع العنوان الأصلي عند إلغاء تحميل المكون
return () => {
document.title = originalTitle;
};
}, [title]); // إعادة التنفيذ عند تغيير الـ title
};
// مثال آخر مع event listener
const 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);
// cleanup function لإزالة الـ event listener
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // لا تعتمد على أي dependencies
return size;
};في مشروع لشركة تقدم خدمات الحجز الفندقي، كان لدينا مكون يعرض قائمة الفنادق بناءً على فلتر البحث. المشكلة كانت أن المستخدمين كانوا يقومون بتغيير الفلاتر بسرعة، مما يؤدي إلى إرسال عدة طلبات API في نفس الوقت. أحياناً كانت الاستجابة الأخيرة تأتي قبل الاستجابة الأولى، مما يؤدي إلى عرض بيانات غير صحيحة. هذا ما يعرف بـ Race Condition، وهو مشكلة شائعة عند التعامل مع الـ async logic في React.
الحل كان في إنشاء Custom Hook اسمه useAsyncData يتعامل مع هذه المشكلة بطريقتين: الأولى هي إلغاء الطلبات السابقة عند إرسال طلب جديد، والثانية هي تجاهل الاستجابات التي لا تتطابق مع أحدث طلب. هذا الـ Hook لم يكن مجرد استخراج للكود، بل كان إعادة تصميم كاملة لكيفية تعاملنا مع البيانات غير المتزامنة. بدلاً من أن يكون المكون مسؤولاً عن إدارة حالة التحميل والأخطاء، أصبح الـ Hook هو المسؤول عن ذلك، ويوفر للمكون واجهة بسيطة لاستخدام البيانات.
// Custom Hook لإدارة البيانات غير المتزامنة مع تجنب Race Conditions
import { useState, useEffect, useRef } from 'react';
const useAsyncData = (asyncFunction, dependencies = []) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const isMounted = useRef(true);
const latestRequestId = useRef(0);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
useEffect(() => {
const requestId = ++latestRequestId.current;
setLoading(true);
setError(null);
asyncFunction(...dependencies)
.then(resp> {
if (isMounted.current && requestId === latestRequestId.current) {
setData(response);
setLoading(false);
}
})
.catch(err => {
if (isMounted.current && requestId === latestRequestId.current) {
setError(err);
setLoading(false);
}
});
}, dependencies);
return { data, loading, error };
};
// مثال على استخدام الـ Hook
const HotelList = ({ location, checkIn, checkOut }) => {
const { data: hotels, loading, error } = useAsyncData(
fetchHotels,
[location, checkIn, checkOut]
);
if (loading) return <div>جاري تحميل الفنادق...</div>;
if (error) return <div>حدث خطأ: {error.message}</div>;
if (!hotels) return <div>لا توجد فنادق متاحة</div>;
return (
<div className="hotel-list">
{hotels.map(hotel => (
<div key={hotel.id} className="hotel-card">
<h3>{hotel.name}</h3>
<p>السعر: {hotel.price} دولار لليلة</p>
</div>
))}
</div>
);
};
// دالة وهمية لاستدعاء API
async function fetchHotels(location, checkIn, checkOut) {
const response = await fetch(`/api/hotels?location=${location}&checkIn=${checkIn}&checkOut=${checkOut}`);
if (!response.ok) {
throw new Error('فشل تحميل البيانات');
}
return response.json();
}عندما بدأت في استخدام Custom Hooks، كنت أعتقد أن مجرد استخراج الكود إلى دالة منفصلة يكفي. لكن مع الوقت، تعلمت أن هناك ممارسات معينة تجعل الفرق بين Custom Hook جيد وآخر يسبب مشاكل لا تنتهي. أولاً، يجب دائماً تسمية الـ Hooks بـ use متبوعاً باسم وصفي، مثل useUserSession بدلاً من getSession. هذا ليس مجرد اصطلاح، بل هو ما يسمح لـ React بالتعرف على الـ Hook وتطبيق قواعدها الخاصة. ثانياً، يجب تجنب استخدام الـ Hooks داخل حلقات أو شروط، لأن هذا يكسر القاعدة الأساسية لـ React في الحفاظ على ترتيب استدعاء الـ Hooks.
ثالثاً، يجب أن تكون الـ Hooks مستقلة قدر الإمكان. مثلاً، إذا كان لديك Custom Hook اسمه useUserProfile يعتمد على useUserSession، فيجب أن يكون هذا الاعتماد واضحاً في الـ dependencies وليس مخفياً داخل الكود. رابعاً، يجب دائماً اختبار الـ Hooks بشكل مستقل عن المكونات، لأن هذا يسمح لك باكتشاف المشاكل مبكراً وتجنب الـ side effects غير المتوقعة. في أحد المشاريع، قمنا بإنشاء مكتبة كاملة من Custom Hooks واختبرناها باستخدام Jest وReact Testing Library، مما ساعدنا في اكتشاف مشاكل في الأداء قبل أن تصل إلى بيئة الإنتاج.
عندما تستدعي Custom Hook داخل مكون React، فإن React لا يعامله كدالة عادية. بدلاً من ذلك، يقوم React بتتبع كل استدعاء لـ Hook داخل المكون، ويحفظ حالة كل hook في ما يعرف بـ Hooks Queue. هذا الـ Queue هو مجرد مصفوفة داخلية تحتفظ بكل حالة من حالات الـ Hooks المرتبطة بالمكون. عند إعادة تنفيذ المكون، يقوم React بالمرور على هذا الـ Queue واستعادة الحالة السابقة لكل hook بناءً على ترتيبه في الـ Queue.
هذا النظام هو ما يسمح لـ React بالحفاظ على حالة الـ Hooks بين عمليات إعادة التنفيذ، لكنه أيضاً ما يجعل تغيير ترتيب استدعاء الـ Hooks خطأً شائعاً. إذا قمت بتغيير ترتيب استدعاء useState وuseEffect مثلاً، فإن React سيفقد القدرة على مطابقة الحالة السابقة مع الـ Hook الصحيح، مما يؤدي إلى سلوك غير متوقع. هذا أيضاً ما يجعل استخدام الـ Hooks داخل شروط أو حلقات خطأً فادحاً، لأن React يعتمد على ترتيب الاستدعاء الثابت للحفاظ على حالة الـ Hooks.
// مثال يوضح كيف يتعامل React مع الـ Hooks داخلياً
// (هذا مجرد توضيح مبسط، وليس الكود الفعلي لـ React)
let currentComp null;
let workInProgressHook = null;
function useState(initialValue) {
// إذا كان هناك hook سابق، استخدمه، وإلا أنشئ واحداً جديداً
const hook = workInProgressHook || {
state: initialValue,
queue: []
};
// إذا كانت هناك تحديثات معلقة، قم بمعالجتها
if (hook.queue.length) {
for (const update of hook.queue) {
hook.state = update(hook.state);
}
hook.queue = [];
}
workInProgressHook = hook.next;
// دالة لتحديث الحالة
const setState = (newState) => {
hook.queue.push(typeof newState === 'function' ? newState : () => newState);
// جدولة إعادة تنفيذ المكون
scheduleUpdate(currentComponent);
};
return [hook.state, setState];
}
// عند تنفيذ المكون، يقوم React بإعداد الـ Hooks Queue
function renderComponent(Component) {
currentComponent = Component;
workInProgressHook = Component.hooksQueue;
const result = Component();
Component.hooksQueue = workInProgressHook;
workInProgressHook = null;
return result;
}إذا كنت تريد أن تبني Custom Hooks قوية وموثوقة، فاتبع هذه القاعدة الذهبية: تعامل مع كل Custom Hook كما لو كان مكوناً مستقلاً تماماً. هذا يعني أنه يجب أن يكون لديه حالة داخلية خاصة به، ويجب أن يدير الـ side effects الخاصة به، ويجب أن يكون قابلاً للاختبار بشكل مستقل. لا تجعل الـ Hook يعتمد على حالة خارجية إلا إذا كانت هذه الحالة تأتي عبر الـ props أو الـ context، ولا تجعل الـ Hook يعدل حالة خارجية بشكل غير متوقع. إذا اتبعت هذه القاعدة، فسوف تجد أن Custom Hooks الخاصة بك تصبح أكثر قوة ومرونة، ويمكن إعادة استخدامها عبر مشاريع مختلفة دون مشاكل.
وأخيراً، تذكر أن Custom Hooks ليست مجرد أداة لتقسيم الكود، بل هي نمط برمجي يسمح لك بإعادة التفكير في كيفية تصميم تطبيقات React. بدلاً من التفكير في المكونات كعناصر واجهة مستخدم فقط، فكر فيها كعناصر تجمع بين واجهة المستخدم ومنطق العمل. وعندما يصبح منطق العمل معقداً، استخرجه إلى Custom Hook. بهذه الطريقة، ستجد نفسك تبني تطبيقات أكثر نظافة، وأكثر قابلية للصيانة، وأكثر كفاءة.