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

الميزة التي سنبنيها داخل التطبيق
في كثير من المواقع التعليمية أو التقنية، يحتاج المستخدم إلى نسخ كود برمجي بسرعة عبر زر مخصص. عند الضغط على الزر، يتم إرسال النص إلى حافظة النظام بحيث يمكن لصقه مباشرة في أي محرر أو مشروع.
غالباً ما تُستخدم مكتبات خارجية مثل react-copy-to-clipboard لتحقيق هذا السلوك، لكن بناء الحل بنفسك يمنحك مرونة أكبر، وفهماً أعمق لكيفية عمل Hooks المخصصة، كما يقلل أحياناً من الاعتماد على حزم إضافية غير ضرورية.

إعادة بناء وظيفة النسخ دون الاعتماد الكامل على مكوّن خارجي
بدلاً من استخدام مكوّن جاهز، سننشئ Hook خاصة بنا داخل ملف مثل useCopyToClipboard.js. ومن الأفضل وضع هذا النوع من الأدوات داخل مجلد مثل utils أو lib حتى تبقى الوظائف القابلة لإعادة الاستخدام مرتبة وسهلة الوصول.
سنستخدم مكتبة موثوقة اسمها copy-to-clipboard لأنها توفر دالة جاهزة لنسخ النصوص بشكل أكثر استقراراً عبر المتصفحات. وتقوم هذه المكتبة بتصدير دالة يمكننا استيرادها باسم copy.
import React from "react";
import copy from "copy-to-clipboard";
export default function useCopyToClipboard() {}
بناء دالة النسخ handleCopy
الخطوة التالية هي إنشاء دالة داخل Hook تتولى تنفيذ النسخ. سنسميها handleCopy. هذه الدالة ستستقبل القيمة المطلوب نسخها، لكن من الأفضل أولاً التأكد من أن نوع البيانات المدخل مناسب.
في هذا المثال، سنسمح فقط بالقيم من النوع string أو number. أما إذا كانت القيمة من نوع آخر، فسنمنع النسخ ونطبع رسالة خطأ في وحدة التحكم console.
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. بعد ذلك نعيد handleCopy من داخل Hook حتى نتمكن من استعمالها في أي مكوّن.
في العادة، يتم ربط هذه الدالة بحدث onClick داخل زر النسخ.
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;
}
إضافة حالة توضح نجاح النسخ
نسخ النص وحده لا يكفي من ناحية تجربة المستخدم. من الأفضل أن نعرض مؤشراً بصرياً يؤكد نجاح العملية، مثل تبديل أيقونة الحافظة إلى أيقونة نجاح.
لذلك سنستخدم useState لإنشاء حالة باسم isCopied، وتكون قيمتها الافتراضية false. وعند نجاح النسخ نغيّرها إلى true. وإذا فشلت العملية أو كانت القيمة غير صالحة، نعيدها إلى false.
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];
}
ما الذي تغير هنا؟
- أضفنا الحالة
isCopiedلتتبع نجاح النسخ. - أصبح
Hookيعيد مصفوفة تحتوي على الحالة والدالة معاً. - صار بالإمكان استخدام هذه الحالة داخل الواجهة لتغيير النص أو الأيقونة.
كيفية استخدام useCopyToClipboard داخل مكوّن
بعد تجهيز Hook، يمكن استعمالها داخل أي مكوّن بسهولة. المثال التالي يوضح زر نسخ يستقبل كوداً عبر الخاصية code. وعند النقر على الزر يتم استدعاء handleCopy لنسخ المحتوى. كما نستخدم الحالة isCopied لتبديل الأيقونة بعد نجاح العملية.
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 تبقى مفعّلة دائماً
إذا تركنا الكود بهذا الشكل، فستبقى قيمة isCopied مساوية لـ true بعد أول عملية نسخ ناجحة. وهذا يعني أن واجهة النجاح ستستمر في الظهور دائماً، وهو سلوك غير مثالي في معظم الحالات.

إضافة مؤقت لإعادة ضبط الحالة تلقائياً
لحل هذه المشكلة، يمكننا تمرير قيمة زمنية إلى useCopyToClipboard تمثل مدة الانتظار قبل إعادة isCopied إلى false. سنسمي هذه القيمة resetInterval، وتكون افتراضياً null. وهذا يعني أن الحالة لن تُعاد تلقائياً ما لم نمرر قيمة صريحة.
سنستخدم useEffect لمراقبة تغيّر isCopied. فإذا أصبحت true وكان هناك resetInterval، سننشئ مؤقتاً عبر setTimeout لإرجاع الحالة بعد المدة المحددة. كما سنحرص على تنظيف المؤقت باستخدام clearTimeout عند إزالة المكوّن من الصفحة، حتى لا نحاول تحديث حالة لم تعد موجودة.
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 داخل useCallback حتى لا تُعاد إنشاء الدالة مع كل إعادة تصيير rerender. هذه الخطوة مفيدة عندما تُمرر الدالة إلى مكونات فرعية أو عندما ترغب في الحفاظ على مرجع ثابت للدالة لتحسين الأداء.
النتيجة النهائية وطريقة الاستخدام العملية
بعد إضافة المؤقت، أصبح بإمكاننا تمرير مدة مثل 3000 ميلي ثانية، أي ثلاث ثوانٍ، ليعود الزر بعد ذلك إلى حالته الأصلية تلقائياً.
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>
);
}

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