كيفية جعل تطبيقات React متجاوبة باستخدام Hook مخصص
عند تطوير واجهات حديثة باستخدام React، لا يكفي أن يبدو التطبيق جيداً على شاشة الحاسوب فقط، بل يجب أن يتكيف بسلاسة مع الهواتف والأجهزة اللوحية ومقاسات العرض المختلفة. أحد الأساليب العملية لتحقيق ذلك هو إنشاء Hook مخصص يقرأ أبعاد نافذة المتصفح ويمنحك تحكماً مباشراً في إظهار العناصر أو إخفائها داخل الواجهة.
في هذا الدليل، سنبني Hook باسم useWindowSize، ثم نستخدمه لجعل مكونات React أكثر مرونة، مع مراعاة التوافق مع البيئات التي تعتمد على التصيير من جهة الخادم مثل Gatsby وNext.js.

لماذا نستخدم Hook مخصصاً لجعل واجهات React متجاوبة؟
عادةً يمكن الاعتماد على CSS media queries للتحكم في شكل العناصر عند تغيّر أبعاد الشاشة، وهذا حل ممتاز في كثير من الحالات. لكن أحياناً نحتاج إلى منطق برمجي داخل JSX نفسه، مثل:
- إخفاء عناصر معينة عندما يقل العرض عن حدّ معين.
- تغيير سلوك مكوّن تفاعلي اعتماداً على عرض الشاشة.
- تمرير أبعاد النافذة إلى مكونات أخرى لاتخاذ قرارات منطقية.
هنا تظهر فائدة إنشاء Hook مخصص يعيد لنا قيمتي العرض والارتفاع بشكل مباشر، بدلاً من الاعتماد على مكتبة خارجية كاملة من أجل وظيفة بسيطة.
فكرة Hook useWindowSize
الهدف من useWindowSize هو إرجاع كائن يحتوي على:
width: عرض نافذة المتصفح الحالي.height: ارتفاع نافذة المتصفح الحالي.
وعند تغيير حجم النافذة، يتم تحديث هذه القيم تلقائياً باستخدام مستمع حدث resize.
إنشاء ملف الـ Hook المخصص
ابدأ بإنشاء ملف جديد داخل مجلد الأدوات، مثلاً utils/useWindowSize.js، ثم أضف الهيكل الأساسي التالي:
// utils/useWindowSize.js
import React from "react";
export default function useWindowSize() {}
في هذه المرحلة، لدينا مجرد دالة مخصصة سنضيف إليها منطق قراءة أبعاد النافذة والتعامل مع تحديثها.
التحدي الأهم: دعم SSR في React
إذا كنت تستخدم إطاراً مثل Gatsby أو Next.js، فهناك نقطة مهمة جداً: الكود قد يُنفّذ أولاً على الخادم، وفي هذه البيئة لن يكون الكائن window متاحاً.
لهذا السبب، لا يمكننا افتراض وجود window دائماً. الحل هو التحقق من البيئة أولاً، ثم استخدام قيم افتراضية مناسبة عند العمل ضمن SSR.
محاولة أولية لفحص window
قد تبدو الفكرة الأولى بالشكل التالي:
// utils/useWindowSize.js
import React from "react";
export default function useWindowSize() {
if (typeof window !== "undefined") {
return { width: 1200, height: 800 };
}
}
لكن هذا الأسلوب غير صحيح عملياً في سياق Hooks، لأن React Hooks يجب أن تُستدعى دائماً بالترتيب نفسه، ولا يجوز وضعها بعد شروط تغيّر مسار التنفيذ.
قراءة أبعاد النافذة باستخدام useState وuseEffect
الأسلوب الصحيح هو تعريف الحالة أولاً، ثم تحديد قيمتها الابتدائية بشكل آمن. بعد ذلك نستخدم useEffect للاستماع إلى حدث resize.
// utils/useWindowSize.js
import React from "react";
export default function useWindowSize() {
if (typeof window !== "undefined") {
return { width: 1200, height: 800 };
}
const [windowSize, setWindowSize] = React.useState();
React.useEffect(() => {
window.addEventListener("resize", () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
});
}, []);
}
هذا المثال يوضح الفكرة العامة: عند تغيير حجم النافذة، يتم تحديث الحالة windowSize باستخدام القيمتين window.innerWidth وwindow.innerHeight.
لكن ما يزال هناك خطأ بنيوي، لأن وجود الشرط قبل useState وuseEffect يخالف قواعد Hooks.
الطريقة الصحيحة لدعم التصيير من جهة الخادم
لحل المشكلة، نحتاج إلى تعريف متغير يحدد ما إذا كانت البيئة الحالية تتيح الوصول إلى window أم لا، ثم نستخدم هذا التحقق داخل القيمة الابتدائية للحالة نفسها، وليس قبل استدعاء Hooks.
// 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 متاحاً، نستخدم أبعاداً افتراضية حتى لا يتعطل التطبيق أثناء التصيير على الخادم.
مهم تقنياً: المتغير isSSR في هذا المثال يمثل نتيجة فحص وجود window، لكن تسمية المتغير قد تكون مضللة. الأفضل في المشاريع العملية اختيار اسم أوضح مثل hasWindow أو isBrowser لتجنب الالتباس.
تنظيف مستمع الأحداث عند إزالة المكوّن
أي مستمع أحداث تتم إضافته داخل useEffect يجب تنظيفه عند إزالة المكوّن من الواجهة، حتى لا يحدث تسرب في الذاكرة أو تتكرر المستمعات دون داعٍ.
يمكن تنفيذ ذلك عبر إرجاع دالة تنظيف من 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
});
});
return () => {
window.removeEventListener("resize", () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
});
};
}, []);
}
لكن هنا توجد مشكلة دقيقة: دالة removeEventListener يجب أن تستقبل المرجع نفسه الذي استُخدم في addEventListener. في المثال السابق تم تمرير دالتين مختلفتين، وبالتالي لن تتم إزالة المستمع كما ينبغي.
الصيغة النهائية الصحيحة لـ useWindowSize
الحل هو تعريف دالة مشتركة واحدة، ثم استخدامها عند الإضافة والإزالة معاً. بعد ذلك نُرجع الحالة في نهاية الـ Hook.
// 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;
}
هذه النسخة تحقق الهدف الأساسي: تزويد أي مكوّن في التطبيق بأبعاد النافذة الحالية، مع تحديث تلقائي عند تغيير الحجم.
ملاحظة تقنية مهمة حول المنطق الشرطي
رغم أن الكود المعروض يحافظ على الفكرة التعليمية للمثال الأصلي، فإن الصياغة الأدق منطقياً عند فحص window تكون عادةً باستخدام شرط يعبّر بوضوح عن وجود الكائن في بيئة المتصفح. لذلك من المفيد دائماً مراجعة أسماء المتغيرات والقيم الافتراضية لضمان وضوح الكود وسهولة صيانته.
كيفية استخدام Hook داخل مكوّن React
بعد إنشاء useWindowSize، يمكن استيراده واستخدامه داخل أي مكوّن. في المثال التالي، سنُظهر بعض الروابط فقط عندما يكون عرض النافذة أكبر من 500px، بينما يبقى زر الاشتراك ظاهراً في جميع الحالات.
// 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>
);
}
هذا النمط مفيد جداً عندما تريد ربط التجاوب بمنطق الواجهة نفسها، وليس فقط بتنسيق CSS. مثلاً يمكنك إخفاء عناصر الملاحة الثانوية على الشاشات الصغيرة، أو تغيير ترتيب العرض حسب المساحة المتاحة.
متى يكون هذا الأسلوب أفضل من media queries؟
استخدام CSS media queries يظل الخيار الأول عندما يكون المطلوب مجرد تغيير بصري، مثل تعديل الهوامش أو أحجام الخطوط أو توزيع العناصر. أما Hook مخصص مثل useWindowSize فيكون أنسب عندما تحتاج إلى منطق برمجي، مثل:
- التحكم في شرط إظهار مكوّن كامل أو إزالته من الشجرة.
- تحميل عناصر أو بيانات معينة فقط على شاشات محددة.
- تبديل سلوك المكوّنات التفاعلية اعتماداً على أبعاد الشاشة.
- مزامنة أبعاد النافذة مع مكتبات الرسم البياني أو التخطيط الديناميكي.
أفضل الممارسات عند بناء Hook للتجاوب
1. تجنب الاعتماد غير الضروري على مكتبات خارجية
إذا كانت حاجتك بسيطة، فإن كتابة Hook صغير مخصص قد يكون أكثر كفاءة ووضوحاً من استيراد مكتبة كاملة.
2. راعِ بيئات SSR من البداية
أي محاولة مباشرة للوصول إلى window أو document قد تتسبب في أخطاء عند التصيير على الخادم. لذا احرص على التحقق من البيئة قبل القراءة منها.
3. نظف مستمعات الأحداث دائماً
إهمال إزالة مستمع resize قد يؤدي إلى سلوك غير متوقع مع الوقت، خصوصاً في التطبيقات التي تحتوي على مكونات كثيرة يتم تركيبها وإزالتها باستمرار.
4. استخدم أسماء متغيرات واضحة
الأسماء مثل windowSize وsetWindowSize وchangeWindowSize تجعل الكود أكثر قابلية للفهم، وهو أمر مهم عند العمل ضمن فرق تطوير أو عند العودة إلى المشروع لاحقاً.
أين يمكن استخدام هذا Hook عملياً؟
- رؤوس الصفحات
Headersالتي تقلل عدد الروابط في الشاشات الصغيرة. - القوائم الجانبية التي تتحول إلى قائمة منسدلة على الهاتف.
- بطاقات المنتجات التي يتغير عددها في الصف الواحد حسب العرض.
- لوحات التحكم
Dashboardsالتي تعيد ترتيب الأدوات بناءً على مساحة الشاشة. - العناصر الترويجية أو أزرار الدعوة لاتخاذ إجراء التي يجب أن تبقى بارزة دائماً.

الخلاصة التقنية
إنشاء Hook مخصص مثل useWindowSize يمنحك طريقة عملية وخفيفة لجعل تطبيقات React أكثر تجاوباً وذكاءً. الميزة الأساسية هنا ليست فقط معرفة عرض الشاشة، بل القدرة على تحويل هذه المعلومة إلى قرارات برمجية مباشرة داخل JSX. وعند مراعاة دعم SSR وتنظيف مستمعات الأحداث بشكل صحيح، ستحصل على حل قابل للتوسع وملائم للمشاريع الحديثة المبنية باستخدام Gatsby أو Next.js أو أي تطبيق React متقدم.