أطلق العنان لقوة `createPortal` في React لإدارة واجهة المستخدم المتقدمة، والنوافذ المنبثقة، وتلميحات الأدوات، والتغلب على قيود z-index.
إتقان تراكبات واجهة المستخدم: نظرة متعمقة على دالة `createPortal` في React
في تطوير الويب الحديث، يعد إنشاء واجهات مستخدم سلسة وبديهية أمرًا بالغ الأهمية. غالبًا ما يتضمن هذا عرض العناصر التي تحتاج إلى الخروج من التسلسل الهرمي لـ DOM الخاص بالمكون الأصل. فكر في مربعات الحوار المنبثقة، أو لافتات الإشعارات، أو تلميحات الأدوات، أو حتى قوائم السياق المعقدة. تتطلب عناصر واجهة المستخدم هذه في كثير من الأحيان معالجة خاصة لضمان عرضها بشكل صحيح، وتراكب فوق المحتوى الآخر دون تدخل من سياقات التراص CSS z-index.
يوفر React، في تطوره المستمر، حلاً قويًا لهذه المشكلة بالتحديد: دالة createPortal. تتيح لك هذه الميزة، المتوفرة من خلال react-dom، عرض المكونات الفرعية في عقدة DOM موجودة خارج التسلسل الهرمي لمكون React العادي. ستكون مشاركة المدونة هذه بمثابة دليل شامل لفهم واستخدام createPortal بفعالية، واستكشاف مفاهيمها الأساسية وتطبيقاتها العملية وأفضل الممارسات لجمهور التطوير العالمي.
ما هو `createPortal` ولماذا تستخدمه؟
في جوهرها، React.createPortal(child, container) هي دالة تعرض مكون React (child) في عقدة DOM مختلفة (container) عن تلك الموجودة كأصل لمكون React في شجرة React.
دعنا نحلل المعلمات:
child: هذا هو عنصر React أو السلسلة أو الجزء الذي تريد عرضه. إنه في الأساس ما سترجعه عادةً من طريقةrenderالخاصة بالمكون.container: هذا هو عنصر DOM موجود في مستندك. إنه الهدف الذي سيتم فيه إلحاقchild.
المشكلة: التسلسل الهرمي لـ DOM وسياقات التراص CSS
ضع في اعتبارك سيناريو شائعًا: مربع حوار منبثق. تهدف النوافذ المنبثقة عادةً إلى عرضها أعلى كل المحتوى الآخر في الصفحة. إذا قمت بعرض مكون منبثق مباشرةً داخل مكون آخر يحتوي على نمط overflow: hidden مقيد أو قيمة z-index معينة، فقد يتم قص النافذة المنبثقة أو تراكبها بشكل غير صحيح. يرجع هذا إلى الطبيعة الهرمية لـ DOM وقواعد سياق التراص z-index الخاصة بـ CSS.
تؤثر قيمة z-index على عنصر ما فقط على ترتيب التراص الخاص به بالنسبة إلى أشقائه ضمن نفس سياق التراص. إذا أنشأ عنصر سلف سياق تراص جديد (على سبيل المثال، عن طريق وجود position بخلاف static وقيمة z-index)، فسيتم حصر العناصر الفرعية المعروضة داخل هذا السلف في هذا السياق. يمكن أن يؤدي هذا إلى مشكلات تخطيط محبطة حيث يتم دفن التراكب المقصود أسفل العناصر الأخرى.
الحل: `createPortal` للإنقاذ
يعمل createPortal على حل هذه المشكلة بأناقة عن طريق كسر الاتصال المرئي بين موضع المكون في شجرة React وموضعه في شجرة DOM. يمكنك عرض مكون داخل بوابة، وسيتم إلحاقه مباشرةً بعقدة DOM وهي شقيق أو طفل من body، متجاوزًا بشكل فعال سياقات التراص الأصلية التي تسبب المشكلات.
على الرغم من أن البوابة تعرض طفلها في عقدة DOM مختلفة، إلا أنها لا تزال تتصرف مثل مكون React عادي داخل شجرة React الخاصة بك. هذا يعني أن انتشار الأحداث يعمل على النحو المتوقع: إذا تم إرفاق معالج أحداث بمكون يعرضه بوابة، فستظل هذه الأحداث تنتقل عبر التسلسل الهرمي لمكون React، وليس فقط التسلسل الهرمي لـ DOM.
حالات الاستخدام الرئيسية لـ `createPortal`
إن تعدد استخدامات createPortal يجعلها أداة لا غنى عنها لأنماط واجهة المستخدم المختلفة:
1. نوافذ الحوار والنوافذ المنبثقة
ربما تكون هذه هي حالة الاستخدام الأكثر شيوعًا وإقناعًا. تم تصميم النوافذ المنبثقة لمقاطعة سير عمل المستخدم والمطالبة بالاهتمام. قد يؤدي عرضها مباشرةً داخل مكون إلى حدوث مشكلات في سياق التراص.
مثال على السيناريو: تخيل تطبيقًا للتجارة الإلكترونية حيث يحتاج المستخدمون إلى تأكيد طلب. يجب أن تظهر نافذة التأكيد المنبثقة فوق كل شيء آخر في الصفحة.
فكرة التنفيذ:
- قم بإنشاء عنصر DOM مخصص في ملف
public/index.html(أو قم بإنشاء واحد بشكل ديناميكي). الممارسة الشائعة هي الحصول على<div id="modal-root"></div>، والتي غالبًا ما يتم وضعها في نهاية العلامة<body>. - في تطبيق React الخاص بك، احصل على مرجع إلى عقدة DOM هذه.
- عندما يتم تشغيل مكون النافذة المنبثقة، استخدم
ReactDOM.createPortalلعرض محتوى النافذة المنبثقة في عقدة DOMmodal-root.
مقتطف التعليمات البرمجية (تصوري):
// App.js
import React from 'react';
import Modal from './Modal';
function App() {
const [isModalOpen, setIsModalOpen] = React.useState(false);
return (
Welcome to Our Global Store!
{isModalOpen && (
setIsModalOpen(false)}>
Confirm Your Purchase
Are you sure you want to proceed?
)}
);
}
export default App;
// Modal.js
import React from 'react';
import ReactDOM from 'react-dom';
const modalRoot = document.getElementById('modal-root');
function Modal({ children, onClose }) {
// Create a DOM element for the modal content to live in
const element = document.createElement('div');
React.useEffect(() => {
// Append the element to the modal root when the component mounts
modalRoot.appendChild(element);
// Clean up by removing the element when the component unmounts
return () => {
modalRoot.removeChild(element);
};
}, [element]);
return ReactDOM.createPortal(
{children}
,
element // Render into the element we created
);
}
export default Modal;
يضمن هذا النهج أن النافذة المنبثقة هي طفل مباشر لـ modal-root، والذي يتم إلحاقه عادةً بـ body، وبالتالي يتجاوز أي سياقات تراص وسيطة.
2. تلميحات الأدوات والنوافذ المنبثقة
تلميحات الأدوات والنوافذ المنبثقة عبارة عن عناصر واجهة مستخدم صغيرة تظهر عندما يتفاعل المستخدم مع عنصر آخر (على سبيل المثال، يحوم فوق زر أو ينقر فوق رمز). يحتاجون أيضًا إلى الظهور فوق المحتوى الآخر، خاصة إذا كان العنصر المشغل متداخلاً بعمق داخل تخطيط معقد.
مثال على السيناريو: في نظام أساسي للتعاون الدولي، يحوم المستخدم فوق صورة رمزية لعضو في الفريق لرؤية تفاصيل الاتصال وحالة التوفر الخاصة به. يجب أن يكون تلميح الأداة مرئيًا بغض النظر عن نمط الحاوية الأصلية للصورة الرمزية.
فكرة التنفيذ: على غرار النوافذ المنبثقة، يمكنك إنشاء بوابة لعرض تلميحات الأدوات. النمط الشائع هو إرفاق تلميح الأداة بجذر بوابة شائع، أو حتى مباشرةً بـ body إذا لم يكن لديك حاوية بوابة معينة.
مقتطف التعليمات البرمجية (تصوري):
// Tooltip.js
import React from 'react';
import ReactDOM from 'react-dom';
function Tooltip({ children, targetElement }) {
if (!targetElement) return null;
// Render the tooltip content directly into the body
return ReactDOM.createPortal(
{children}
,
document.body
);
}
// Parent Component that triggers the tooltip
function InfoButton({ info }) {
const [targetRef, setTargetRef] = React.useState(null);
const [showTooltip, setShowTooltip] = React.useState(false);
return (
setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
style={{ position: 'relative', display: 'inline-block' }}
>
? {/* Information icon */}
{showTooltip && {info} }
);
}
3. قوائم منسدلة ومربعات تحديد
يمكن أن تستفيد القوائم المنسدلة المخصصة ومربعات التحديد أيضًا من البوابات. عند فتح قائمة منسدلة، غالبًا ما تحتاج إلى التمدد خارج حدود الحاوية الأصلية، خاصةً إذا كانت هذه الحاوية تحتوي على خصائص مثل overflow: hidden.
مثال على السيناريو: تتميز لوحة معلومات داخلية لشركة متعددة الجنسيات بقائمة منسدلة تحديد مخصصة لاختيار مشروع من قائمة طويلة. يجب ألا يقتصر طول أو عرض القائمة المنسدلة على أداة لوحة المعلومات التي توجد فيها.
فكرة التنفيذ: قم بعرض خيارات القائمة المنسدلة في بوابة مرفقة بـ body أو جذر بوابة مخصص.
4. أنظمة الإشعارات
تعد أنظمة الإشعارات العالمية (رسائل التحميص والتنبيهات) مرشحًا ممتازًا آخر لـ createPortal. تظهر هذه الرسائل عادةً في موضع ثابت، غالبًا في أعلى الشاشة أو أسفلها، بغض النظر عن موضع التمرير الحالي أو تخطيط المكون الأصل.
مثال على السيناريو: يعرض موقع حجز رحلات رسائل تأكيد للحجوزات الناجحة أو رسائل خطأ للمدفوعات الفاشلة. يجب أن تظهر هذه الإشعارات باستمرار على شاشة المستخدم.
فكرة التنفيذ: يمكن استخدام حاوية إشعارات مخصصة (على سبيل المثال، <div id="notifications-root"></div>) مع createPortal.
كيفية تنفيذ `createPortal` في React
يتضمن تنفيذ createPortal بضع خطوات رئيسية:
الخطوة 1: تحديد أو إنشاء عقدة DOM مستهدفة
أنت بحاجة إلى عنصر DOM خارج جذر React القياسي ليكون بمثابة حاوية لمحتوى البوابة. الممارسة الأكثر شيوعًا هي تحديد هذا في ملف HTML الرئيسي (على سبيل المثال، public/index.html).
<!-- public/index.html -->
<body>
<noscript>You need JavaScript enabled to run this app.</noscript>
<div id="root"></div>
<div id="modal-root"></div> <!-- For modals -->
<div id="tooltip-root"></div> <!-- Optionally for tooltips -->
</body>
بدلاً من ذلك، يمكنك إنشاء عنصر DOM ديناميكيًا ضمن دورة حياة تطبيقك باستخدام JavaScript، كما هو موضح في مثال Modal أعلاه، ثم إلحاقه بـ DOM. ومع ذلك، فإن التحديد المسبق في HTML يكون بشكل عام أنظف لجذور البوابة الدائمة.
الخطوة 2: احصل على مرجع إلى عقدة DOM المستهدفة
في مكون React الخاص بك، ستحتاج إلى الوصول إلى عقدة DOM هذه. يمكنك القيام بذلك باستخدام document.getElementById() أو document.querySelector().
// Somewhere in your component or utility file
const modalRootElement = document.getElementById('modal-root');
const tooltipRootElement = document.getElementById('tooltip-root');
// It's crucial to ensure these elements exist before attempting to use them.
// You might want to add checks or handle cases where they are not found.
الخطوة 3: استخدم `ReactDOM.createPortal`
قم باستيراد ReactDOM واستخدم الدالة createPortal، وقم بتمرير JSX الخاص بالمكون الخاص بك كالحجة الأولى وعقدة DOM المستهدفة كالحجة الثانية.
مثال: عرض رسالة بسيطة في بوابة
// MessagePortal.js
import React from 'react';
import ReactDOM from 'react-dom';
function MessagePortal({ message }) {
const portalContainer = document.getElementById('modal-root'); // Assuming you're using modal-root for this example
if (!portalContainer) {
console.error('Portal container "modal-root" not found!');
return null;
}
return ReactDOM.createPortal(
<div style={{ position: 'fixed', bottom: '20px', left: '50%', transform: 'translateX(-50%)', backgroundColor: 'rgba(0,0,0,0.7)', color: 'white', padding: '10px', borderRadius: '5px' }}>
{message}
</div>,
portalContainer
);
}
export default MessagePortal;
// In another component...
function Dashboard() {
return (
<div>
<h1>Dashboard Overview</h1>
<MessagePortal message="Data successfully synced!" />
</div>
);
}
إدارة الحالة والأحداث باستخدام البوابات
تتمثل إحدى أهم مزايا createPortal في أنها لا تعطل نظام معالجة الأحداث الخاص بـ React. ستستمر الأحداث من العناصر المعروضة داخل البوابة في الانتشار عبر شجرة مكون React، وليس فقط شجرة DOM.
مثال على السيناريو: قد يحتوي مربع حوار منبثق على نموذج. عندما ينقر المستخدم فوق زر داخل النافذة المنبثقة، يجب أن يتم التعامل مع حدث النقر بواسطة مستمع أحداث في المكون الأصل الذي يتحكم في رؤية النافذة المنبثقة، وليس محصورًا داخل التسلسل الهرمي لـ DOM للنافذة المنبثقة نفسها.
مثال توضيحي:
// ModalWithEventHandling.js
import React from 'react';
import ReactDOM from 'react-dom';
const modalRoot = document.getElementById('modal-root');
function ModalWithEventHandling({ children, onClose }) {
const modalContentRef = React.useRef(null);
// Using useEffect to create and clean up the DOM element
const [wrapperElement] = React.useState(() => document.createElement('div'));
React.useEffect(() => {
modalRoot.appendChild(wrapperElement);
return () => {
modalRoot.removeChild(wrapperElement);
};
}, [wrapperElement]);
// Handle clicks outside the modal content to close it
const handleOutsideClick = (event) => {
if (modalContentRef.current && !modalContentRef.current.contains(event.target)) {
onClose();
}
};
return ReactDOM.createPortal(
{children}
,
wrapperElement
);
}
// App.js (using the modal)
function App() {
const [showModal, setShowModal] = React.useState(false);
return (
App Content
{showModal && (
setShowModal(false)}>
Important Information
This is content inside the modal.
)}
);
}
في هذا المثال، يؤدي النقر فوق الزر Close Modal إلى استدعاء الدعائم onClose بشكل صحيح التي تم تمريرها من المكون الأصل App. وبالمثل، إذا كان لديك مستمع أحداث للنقرات على modal-backdrop، فسيؤدي ذلك إلى تشغيل الدالة handleOutsideClick بشكل صحيح، على الرغم من عرض النافذة المنبثقة في شجرة فرعية منفصلة لـ DOM.
الأنماط والاعتبارات المتقدمة
البوابات الديناميكية
يمكنك إنشاء وإزالة حاويات البوابة ديناميكيًا بناءً على احتياجات تطبيقك، على الرغم من أن الحفاظ على جذور البوابة الدائمة والمحددة مسبقًا غالبًا ما يكون أبسط.
البوابات والعرض من جانب الخادم (SSR)
عند العمل باستخدام العرض من جانب الخادم (SSR)، يجب أن تضع في اعتبارك كيفية تفاعل البوابات مع HTML الأولي. نظرًا لأن البوابات تعرض في عقد DOM التي قد لا تكون موجودة على الخادم، فأنت غالبًا ما تحتاج إلى عرض محتوى البوابة بشكل مشروط أو التأكد من وجود عقد DOM المستهدفة في ناتج SSR.
النمط الشائع هو استخدام هوك مثل useIsomorphicLayoutEffect (أو هوك مخصص يعطي الأولوية لـ useLayoutEffect على العميل والعودة إلى useEffect على الخادم) لضمان حدوث معالجة DOM فقط على العميل.
// usePortal.js (a common utility hook pattern)
import React, { useRef, useEffect } from 'react';
function usePortal(id) {
const modalRootRef = useRef(null);
useEffect(() => {
let currentModalRoot = document.getElementById(id);
if (!currentModalRoot) {
currentModalRoot = document.createElement('div');
currentModalRoot.setAttribute('id', id);
document.body.appendChild(currentModalRoot);
}
modalRootRef.current = currentModalRoot;
// Cleanup function to remove the created element if it was created by this hook
return () => {
// Be cautious with cleanup; only remove if it was actually created here
// A more robust approach might involve tracking element creation.
};
}, [id]);
return modalRootRef.current;
}
export default usePortal;
// Modal.js (using the hook)
import React from 'react';
import ReactDOM from 'react-dom';
import usePortal from './usePortal';
function Modal({ children, onClose }) {
const portalTarget = usePortal('modal-root'); // Use our hook
if (!portalTarget) return null;
return ReactDOM.createPortal(
e.stopPropagation()}> {/* Prevent closing by clicking inside */}
{children}
,
portalTarget
);
}
بالنسبة إلى SSR، ستحتاج عادةً إلى التأكد من أن div modal-root موجود في HTML المعروض من جانب الخادم. سيقوم تطبيق React على العميل بعد ذلك بإرفاقه به.
تصفيف البوابات
يتطلب تصفيف العناصر داخل البوابة دراسة متأنية. نظرًا لأنها غالبًا ما تكون خارج سياق التصفيف المباشر للأصل، يمكنك تطبيق أنماط عامة أو استخدام وحدات CSS/مكونات ذات نمط لإدارة مظهر محتوى البوابة بفعالية.
بالنسبة إلى التراكبات مثل النوافذ المنبثقة، ستحتاج غالبًا إلى أنماط:
- إصلاح العنصر في إطار العرض (
position: fixed). - امتداد إطار العرض بأكمله (
top: 0; left: 0; width: 100%; height: 100%;). - استخدم قيمة
z-indexعالية للتأكد من ظهورها في الأعلى فوق كل شيء آخر. - تضمين خلفية شبه شفافة للخلفية.
إمكانية الوصول
عند تنفيذ النوافذ المنبثقة أو التراكبات الأخرى، تعد إمكانية الوصول أمرًا بالغ الأهمية. تأكد من إدارة التركيز بشكل صحيح:
- عندما يتم فتح نافذة منبثقة، قم بحبس التركيز داخل النافذة المنبثقة. يجب ألا يتمكن المستخدمون من التمرير خارجها.
- عندما تغلق النافذة المنبثقة، أعد التركيز إلى العنصر الذي قام بتشغيلها.
- استخدم سمات ARIA (على سبيل المثال،
role="dialog"،aria-modal="true"،aria-labelledby،aria-describedby) لإعلام التقنيات المساعدة بطبيعة النافذة المنبثقة.
توفر مكتبات مثل Reach UI أو Material-UI غالبًا مكونات منبثقة يمكن الوصول إليها والتي تتعامل مع هذه المخاوف نيابةً عنك.
المزالق المحتملة وكيفية تجنبها
نسيان عقدة DOM المستهدفة
الخطأ الأكثر شيوعًا هو نسيان إنشاء عقدة DOM المستهدفة في HTML الخاص بك أو عدم الإشارة إليها بشكل صحيح في JavaScript الخاص بك. تأكد دائمًا من وجود حاوية البوابة قبل محاولة العرض فيها.
انتشار الأحداث مقابل انتشار DOM
بينما تنتشر أحداث React بشكل صحيح عبر البوابات، فإن أحداث DOM الأصلية لا تفعل ذلك. إذا كنت تقوم بإرفاق مستمعي أحداث DOM الأصلية مباشرةً بالعناصر الموجودة داخل البوابة، فسيتم انتشارهم فقط عبر شجرة DOM، وليس شجرة مكون React. التزم بنظام الأحداث الاصطناعية الخاص بـ React كلما أمكن ذلك.
تداخل البوابات
إذا كان لديك أنواع متعددة من التراكبات (النوافذ المنبثقة، وتلميحات الأدوات، والإشعارات) والتي يتم عرضها كلها على الجسم أو جذر شائع، فقد تصبح إدارة ترتيب التراص الخاص بها معقدة. يمكن أن يساعد تخصيص قيم z-index معينة أو استخدام نظام إدارة البوابة.
اعتبارات الأداء
في حين أن createPortal نفسه فعال، فإن عرض المكونات المعقدة داخل البوابات لا يزال بإمكانه التأثير على الأداء. تأكد من تحسين محتوى البوابة، وتجنب عمليات إعادة العرض غير الضرورية.
بدائل `createPortal`
في حين أن createPortal هو الطريقة الاصطلاحية لـ React للتعامل مع هذه السيناريوهات، فمن الجدير بالذكر الأساليب الأخرى التي قد تواجهها أو تفكر فيها:
- معالجة DOM المباشرة: يمكنك إنشاء عناصر DOM وإلحاقها يدويًا باستخدام
document.createElementوappendChild، ولكن هذا يتجاوز العرض التصريحي وإدارة الحالة الخاصة بـ React، مما يجعله أقل قابلية للصيانة. - مكونات الترتيب الأعلى (HOCs) أو خصائص العرض: يمكن لهذه الأنماط تجريد منطق عرض البوابة، لكن
createPortalنفسه هو الآلية الأساسية. - مكتبات المكونات: توفر العديد من مكتبات مكونات واجهة المستخدم (على سبيل المثال، Material-UI، Ant Design، Chakra UI) مكونات منبثقة وتلميحات أدوات وقوائم منسدلة مُنشأة مسبقًا والتي تجرّد استخدام
createPortal، مما يوفر تجربة مطور أكثر ملاءمة. ومع ذلك، فإن فهمcreatePortalأمر بالغ الأهمية لتخصيص هذه المكونات أو إنشاء المكونات الخاصة بك.
الخلاصة
React.createPortal هي ميزة قوية وأساسية لبناء واجهات مستخدم متطورة في React. من خلال السماح لك بعرض المكونات في عقد DOM خارج التسلسل الهرمي لشجرة React الخاصة بها، فإنها تحل بشكل فعال المشكلات الشائعة المتعلقة بـ CSS z-index، وسياقات التراص، وتجاوز العنصر.
سواء كنت تقوم ببناء مربعات حوار منبثقة معقدة لتأكيد المستخدم، أو تلميحات أدوات دقيقة للحصول على معلومات سياقية، أو لافتات إشعارات مرئية عالميًا، فإن createPortal يوفر المرونة والتحكم اللازمين. تذكر إدارة عقد DOM الخاصة بالبوابة، والتعامل مع الأحداث بشكل صحيح، وإعطاء الأولوية لإمكانية الوصول والأداء للحصول على تطبيق قوي حقًا وسهل الاستخدام، ومناسب لجمهور عالمي ذي خلفيات واهتمامات فنية متنوعة.
سيؤدي إتقان createPortal بلا شك إلى رفع مستوى مهارات تطوير React الخاصة بك، مما يتيح لك إنشاء واجهات مستخدم أكثر صقلًا واحترافية تبرز في المشهد المعقد بشكل متزايد لتطبيقات الويب الحديثة.