كيفية إنشاء React Hooks مخصصة: دليل عملي خطوة بخطوة
تُعد React Hooks المخصصة من أهم الأدوات التي تمنح مطوّر React مرونة كبيرة في بناء وظائف قابلة لإعادة الاستخدام داخل التطبيق. ففي كثير من الحالات، يمكن تثبيت مكتبة خارجية لحل مشكلة محددة بسرعة، لكن ماذا لو لم تكن هناك مكتبة مناسبة، أو كانت ثقيلة، أو لا تدعم بيئتك التقنية مثل SSR؟ هنا تظهر أهمية إنشاء Hook مخصص يلائم احتياجات المشروع بدقة.
في هذا الدليل العملي، سنستعرض كيفية بناء ثلاثة Hooks مخصصة في React، مع شرح المشكلة التي يحلها كل واحد منها، وطريقة كتابته، وأفضل الممارسات المتعلقة بالأداء والتنظيف وإعادة الاستخدام.

لماذا تحتاج إلى إنشاء React Hooks مخصصة؟
الهدف من Custom Hooks ليس فقط كتابة كود أقل، بل أيضاً:
- عزل المنطق المتكرر في مكان واحد.
- تحسين قابلية الصيانة والتوسعة.
- تقليل الاعتماد على مكتبات خارجية غير ضرورية.
- حل مشكلات خاصة بالمشروع لا توفرها الأدوات الجاهزة.
- التحكم الكامل في الأداء والسلوك داخل التطبيق.
والميزة الأهم هي أنك عندما تبني Hook بنفسك، فإنك تفهم آلية العمل الداخلية بدلاً من الاكتفاء باستخدام حل جاهز بشكل أعمى.
1. بناء useCopyToClipboard لنسخ النص إلى الحافظة
يُستخدم هذا Hook عندما تريد تمكين المستخدم من نسخ نص أو شيفرة برمجية بنقرة واحدة، وهي وظيفة شائعة جداً في المدونات التقنية وصفحات التوثيق ولوحات التحكم.

فكرة Hook ومكان حفظه
من الأفضل وضع هذا النوع من الأدوات في مجلد مثل utils أو lib حتى يكون من السهل إعادة استخدامها في أكثر من جزء داخل التطبيق.
سننشئ ملفاً باسم useCopyToClipboard.js ونبدأ بالهيكل الأساسي التالي:
// utils/useCopyToClipboard.js
import React from "react";
import copy from "copy-to-clipboard";
export default function useCopyToClipboard() {}
إنشاء الدالة handleCopy
نحتاج إلى دالة تستقبل النص المطلوب نسخه. ومن المهم هنا التحقق من نوع البيانات قبل تنفيذ النسخ، حتى لا نحاول نسخ قيم غير صالحة مثل الكائنات أو المصفوفات مباشرة.
import React from "react";
import copy from "copy-to-clipboard";
export default function useCopyToClipboard() {
const [isCopied, setCopied] = React.useState(false);
function handleCopy(text) {
if (typeof text === "string" || typeof text == "number") {
// copy
} else {
// don't copy
console.error(`Cannot copy typeof ${typeof text} to clipboard, must be a string or number.`);
}
}
}
بعد التحقق من النوع، يمكن تحويل القيمة إلى نص باستخدام toString() ثم تمريرها إلى الدالة copy.
import React from "react";
import copy from "copy-to-clipboard";
export default function useCopyToClipboard() {
function handleCopy(text) {
if (typeof text === "string" || typeof text == "number") {
copy(text.toString());
} else {
console.error(`Cannot copy typeof ${typeof text} to clipboard, must be a string or number.`);
}
}
return handleCopy;
}
إضافة حالة توضح نجاح النسخ
من الناحية العملية، لا يكفي تنفيذ النسخ فقط، بل من المفيد أن نعرف هل تمت العملية بنجاح أم لا، حتى نعرض للمستخدم مؤشراً بصرياً مناسباً مثل تغيير الأيقونة أو النص.
import React from "react";
import copy from "copy-to-clipboard";
export default function useCopyToClipboard(resetInterval = null) {
const [isCopied, setCopied] = React.useState(false);
function handleCopy(text) {
if (typeof text === "string" || typeof text == "number") {
copy(text.toString());
setCopied(true);
} else {
setCopied(false);
console.error(`Cannot copy typeof ${typeof text} to clipboard, must be a string or number.`);
}
}
return [isCopied, handleCopy];
}
طريقة استخدام useCopyToClipboard
يمكن استدعاء هذا Hook داخل أي مكوّن، ثم ربط الدالة handleCopy بحدث onClick داخل زر النسخ.
import React from "react";
import ClipboardIcon from "../svg/ClipboardIcon";
import SuccessIcon from "../svg/SuccessIcon";
import useCopyToClipboard from "../utils/useCopyToClipboard";
function CopyButton({ code }) {
const [isCopied, handleCopy] = useCopyToClipboard();
return (
<button onClick={() => handleCopy(code)}>
{isCopied ? <SuccessIcon /> : <ClipboardIcon />}
</button>
);
}
إعادة ضبط الحالة بعد مدة زمنية
لو تركنا القيمة isCopied على true دائماً، فستبقى واجهة النجاح ظاهرة باستمرار. لهذا يمكننا إضافة وسيط اختياري اسمه resetInterval لإعادة الحالة إلى false بعد عدد معيّن من المللي ثانية.

import React from "react";
import copy from "copy-to-clipboard";
export default function useCopyToClipboard(resetInterval = null) {
const [isCopied, setCopied] = React.useState(false);
const handleCopy = React.useCallback((text) => {
if (typeof text === "string" || typeof text == "number") {
copy(text.toString());
setCopied(true);
} else {
setCopied(false);
console.error(`Cannot copy typeof ${typeof text} to clipboard, must be a string or number.`);
}
}, []);
React.useEffect(() => {
let timeout;
if (isCopied && resetInterval) {
timeout = setTimeout(() => setCopied(false), resetInterval);
}
return () => {
clearTimeout(timeout);
};
}, [isCopied, resetInterval]);
return [isCopied, handleCopy];
}
لاحظ هنا استخدام useCallback حتى لا يعاد إنشاء الدالة handleCopy عند كل إعادة تصيير، وهو تحسين مفيد خاصة إذا كانت الدالة تُمرّر إلى مكوّنات أخرى.
النتيجة النهائية
import React from "react";
import ClipboardIcon from "../svg/ClipboardIcon";
import SuccessIcon from "../svg/SuccessIcon";
import useCopyToClipboard from "../utils/useCopyToClipboard";
function CopyButton({ code }) {
// isCopied is reset after 3 second timeout
const [isCopied, handleCopy] = useCopyToClipboard(3000);
return (
<button onClick={() => handleCopy(code)}>
{isCopied ? <SuccessIcon /> : <ClipboardIcon />}
</button>
);
}

2. بناء usePageBottom لاكتشاف الوصول إلى نهاية الصفحة
يُستخدم هذا Hook في حالات كثيرة، أبرزها ميزة التمرير اللانهائي Infinite Scroll، حيث تحتاج إلى تحميل محتوى جديد بمجرد وصول المستخدم إلى أسفل الصفحة.

الهيكل الأساسي للملف
// utils/usePageBottom.js
import React from "react";
export default function usePageBottom() {}
كيف نحدد أن المستخدم وصل إلى آخر الصفحة؟
يمكن حساب ذلك بمقارنة مجموع window.innerHeight وdocument.documentElement.scrollTop مع القيمة document.documentElement.offsetHeight. إذا تساوت القيمتان، فهذا يعني أن المستخدم بلغ النهاية.
// utils/usePageBottom.js
import React from "react";
export default function usePageBottom() {
React.useEffect(() => {}, []);
}
// utils/usePageBottom.js
import React from "react";
export default function usePageBottom() {
React.useEffect(() => {
window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight;
}, []);
}
تخزين النتيجة داخل الحالة
// utils/usePageBottom.js
import React from "react";
export default function usePageBottom() {
const [bottom, setBottom] = React.useState(false);
React.useEffect(() => {
const isBottom = window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight;
setBottom(isButton);
}, []);
return bottom;
}
لكن هذه الصيغة لا تكفي، لأن الحساب يجب أن يتكرر مع كل عملية تمرير، وليس مرة واحدة فقط.
الاستماع لحدث scroll
الحل هو إضافة مستمع لحدث scroll وإعادة تقييم الشرط كلما تحرك المستخدم داخل الصفحة.
// utils/usePageBottom.js
import React from "react";
export default function usePageBottom() {
const [bottom, setBottom] = React.useState(false);
React.useEffect(() => {
function handleScroll() {
const isBottom = window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight;
setBottom(isButton);
}
window.addEventListener("scroll", handleScroll);
}, []);
return bottom;
}
تنظيف مستمع الحدث عند إزالة المكوّن
أي event listener تضيفه داخل useEffect يجب أن تزيله عند unmount لتفادي تسرب الذاكرة أو محاولة تحديث حالة لم تعد موجودة.
// utils/usePageBottom.js
import React from "react";
export default function usePageBottom() {
const [bottom, setBottom] = React.useState(false);
React.useEffect(() => {
function handleScroll() {
const isBottom = window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight;
setBottom(isButton);
}
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
return bottom;
}
فكرة هذا Hook بسيطة، لكنها مفيدة جداً في صفحات المقالات الطويلة، ومتاجر التجارة الإلكترونية، وتطبيقات الشبكات الاجتماعية.
3. بناء useWindowSize لمعرفة أبعاد نافذة المتصفح
أحياناً تحتاج إلى إخفاء عناصر أو إظهارها حسب عرض النافذة، ليس فقط عبر CSS، بل من داخل JSX نفسه. عندها يصبح وجود Hook يعيد أبعاد النافذة أمراً عملياً للغاية.
إنشاء الملف الأساسي
// utils/useWindowSize.js
import React from "react";
export default function useWindowSize() {}
مراعاة تطبيقات SSR مثل Gatsby وNext.js
في التطبيقات التي تعتمد على التصيير من جهة الخادم، قد لا يكون الكائن window متاحاً أثناء التنفيذ الأولي. لذلك لا بد من التعامل مع هذه الحالة بحذر.
// utils/useWindowSize.js
import React from "react";
export default function useWindowSize() {
if (typeof window !== "undefined") {
return { width: 1200, height: 800 };
}
}
لكن هذا الأسلوب غير صحيح مع قواعد Hooks، لأن useState وuseEffect لا يجوز استدعاؤهما بشكل شرطي.
تعيين القيمة الابتدائية بشكل آمن
// utils/useWindowSize.js
import React from "react";
export default function useWindowSize() {
// if (typeof window !== "undefined") {
// return { width: 1200, height: 800 };
// }
const isSSR = typeof window !== "undefined";
const [windowSize, setWindowSize] = React.useState({
width: isSSR ? 1200 : window.innerWidth,
height: isSSR ? 800 : window.innerHeight,
});
React.useEffect(() => {
window.addEventListener("resize", () => {
setWindowSize({ width: window.innerWidth, height: window.innerHeight });
});
}, []);
}
الفكرة هنا هي توفير قيم افتراضية عند غياب window، ثم تحديث الأبعاد الفعلية عند تشغيل الكود في المتصفح.
إزالة مستمع resize بشكل صحيح
عند إضافة مستمع لحدث resize، يجب استخدام مرجع الدالة نفسها عند الإزالة، لا دالة جديدة مختلفة.
// utils/useWindowSize.js
import React from "react";
export default function useWindowSize() {
const isSSR = typeof window !== "undefined";
const [windowSize, setWindowSize] = React.useState({
width: isSSR ? 1200 : window.innerWidth,
height: isSSR ? 800 : window.innerHeight,
});
function changeWindowSize() {
setWindowSize({ width: window.innerWidth, height: window.innerHeight });
}
React.useEffect(() => {
window.addEventListener("resize", changeWindowSize);
return () => {
window.removeEventListener("resize", changeWindowSize);
};
}, []);
return windowSize;
}
مثال على الاستخدام
// components/StickyHeader.js
import React from "react";
import useWindowSize from "../utils/useWindowSize";
function StickyHeader() {
const { width } = useWindowSize();
return (
<div>
{/* visible only when window greater than 500px */}
{width > 500 && (
<>
<div onClick={onTestimonialsClick} role="button">
<span>Testimonials</span>
</div>
<div onClick={onPriceClick} role="button">
<span>Price</span>
</div>
<div>
<span onClick={onQuestionClick} role="button">Question?</span>
</div>
</>
)}
{ /* visible at any window size */ }
<div>
<span className="primary-button" onClick={onPriceClick} role="button">
Join Now
</span>
</div>
</div>
);
}
هذا Hook مفيد جداً عندما تريد الجمع بين مرونة JavaScript واستجابة الواجهة بدل الاعتماد الكامل على media queries.
4. بناء useDeviceDetect لاكتشاف الأجهزة المحمولة
أحياناً قد تحتاج إلى معرفة ما إذا كان المستخدم يتصفح من هاتف أو جهاز لوحي من أجل تعديل بعض عناصر الواجهة أو تعطيل أجزاء معينة لا تعمل جيداً على الشاشات الصغيرة.

في بعض المشاريع، قد تواجه مكتبات مثل react-device-detect مشكلات عند العمل مع SSR، لذا قد يكون الحل الأفضل إنشاء بديل مخصص أبسط وأكثر موثوقية.
الهيكل الأولي
// utils/useDeviceDetect.js
import React from "react";
export default function useDeviceDetect() {}
الحصول على userAgent
يمكن الاستدلال على نوع الجهاز من خلال القيمة navigator.userAgent. وبما أن التعامل مع window وnavigator يُعد أثراً جانبياً، فمن المناسب تنفيذ ذلك داخل useEffect.
// utils/useDeviceDetect.js
import React from "react";
export default function useDeviceDetect() {
React.useEffect(() => {
console.log(`user's device is: ${window.navigator.userAgent} `);
// can also be written as 'navigator.userAgent'
}, []);
}
التحقق من بيئة التشغيل
// utils/useDeviceDetect.js
import React from "react";
export default function useDeviceDetect() {
React.useEffect(() => {
const userAgent = typeof navigator === "undefined" ? "" : navigator.userAgent;
}, []);
}
مطابقة نوع الجهاز عبر Regex
إذا احتوت سلسلة userAgent على أسماء أجهزة أو منصات مثل Android أو iPhone أو iPad، فيمكن اعتبار الجهاز محمولاً.
// utils/useDeviceDetect.js
import React from "react";
export default function useDeviceDetect() {
const [isMobile, setMobile] = React.useState(false);
React.useEffect(() => {
const userAgent = typeof window.navigator === "undefined" ? "" : navigator.userAgent;
const mobile = Boolean(
userAgent.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i)
);
setMobile(mobile);
}, []);
}
إرجاع النتيجة من Hook
من الأفضل إرجاع كائن بدلاً من قيمة واحدة مباشرة، حتى يسهل إضافة خصائص أخرى مستقبلاً مثل isTablet أو isDesktop.
// utils/useDeviceDetect.js
import React from "react";
export default function useDeviceDetect() {
const [isMobile, setMobile] = React.useState(false);
React.useEffect(() => {
const userAgent = typeof window.navigator === "undefined" ? "" : navigator.userAgent;
const mobile = Boolean(
userAgent.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i)
);
setMobile(mobile);
}, []);
return { isMobile };
}
مثال على الاستخدام
// templates/course.js
import React from "react";
import useDeviceDetect from "../utils/useDeviceDetect";
function Course() {
const { isMobile } = useDeviceDetect();
return (
<>
<SEO />
{!isMobile && <StickyHeader {...courseData} />}
{/* more components... */}
</>
);
}
بهذا الشكل تحصل على حل خفيف وواضح ومخصص لاحتياجاتك، دون الارتباط بسلوك مكتبة خارجية قد لا تكون متوافقة مع بنية المشروع.
مقارنة سريعة بين Hooks الثلاثة
اسم Hook |
وظيفته | أبرز فائدة |
|---|---|---|
useCopyToClipboard |
نسخ النص إلى الحافظة | تحسين تجربة المستخدم في صفحات الشيفرة والمحتوى التقني |
usePageBottom |
اكتشاف الوصول إلى أسفل الصفحة | دعم التمرير اللانهائي وتحميل البيانات تدريجياً |
useWindowSize |
معرفة أبعاد نافذة المتصفح | بناء واجهات متجاوبة بمنطق برمجي مرن |
useDeviceDetect |
اكتشاف الأجهزة المحمولة | تخصيص الواجهة حسب نوع الجهاز مع دعم أفضل لـSSR |
أفضل الممارسات عند إنشاء Custom Hooks
- ابدأ الاسم دائماً بكلمة
useحتى يتعرفReactعليه كـHook. - اجعل كل
Hookمسؤولاً عن مهمة واحدة واضحة. - أزل أي
event listenersأو مؤقتات مثلsetTimeoutعند إنهاء المكوّن. - راعِ التوافق مع بيئات
SSRإذا كان مشروعك يستخدمNext.jsأوGatsby. - استخدم
useCallbackأوuseMemoعند الحاجة فقط، وليس بشكل عشوائي. - اختبر
Hookفي أكثر من سيناريو قبل اعتماده داخل التطبيق.
متى تُفضّل إنشاء Hook مخصص بدلاً من تثبيت مكتبة؟
- عندما تكون الوظيفة المطلوبة بسيطة ويمكن تنفيذها بعدد محدود من الأسطر.
- عندما تكون المكتبة الخارجية كبيرة الحجم مقارنة بالفائدة الفعلية.
- عندما تحتاج إلى دعم خاص لمشروعك مثل
SSRأو سلوك مخصص. - عندما ترغب في تقليل التبعيات الخارجية لتسهيل الصيانة مستقبلاً.
- عندما تحتاج إلى فهم كامل للمنطق البرمجي وعدم تركه لصندوق أسود.
ومع ذلك، لا يعني هذا أن المكتبات الخارجية سيئة دائماً؛ بل المقصود أن تختار الحل المناسب حسب حجم المشكلة واحتياجات المشروع.

الخلاصة التقنية
إن إنشاء React Hooks مخصصة ليس مجرد أسلوب لتنظيم الكود، بل هو مهارة أساسية تعكس نضج المطوّر وقدرته على بناء حلول عملية ومرنة. الأمثلة مثل useCopyToClipboard وusePageBottom وuseWindowSize وuseDeviceDetect توضح كيف يمكن تحويل منطق متكرر أو مشكلة تقنية إلى وحدة قابلة لإعادة الاستخدام وسهلة الاختبار. تقنياً، أفضل Hook هو الذي يكون صغيراً، واضح المسؤولية، وآمناً من ناحية الأداء والتنظيف، مع مراعاة التوافق مع بيئات مثل SSR. إذا أتقنت هذا النمط، فستتمكن من بناء تطبيقات React أكثر احترافية وأقل اعتماداً على حلول خارجية لا تحتاجها فعلاً.