Menu
flag Ar iconالعربيةdown icon

البوابات في React: createPortal للنوافذ المنبثقة والتلميحات

يعرض createPortal جزءًا من مكوّن داخل عقدة DOM مختلفة، مثل document.body، بينما يبقى في المكان نفسه من شجرة React. استخدمه للنوافذ المنبثقة والتلميحات والقوائم التي يجب أن تفلت من overflow hidden وتراكب z-index.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

تعرض البوابة (portal) جزءًا من مكوّن في مكان مختلف من DOM، عادة document.body، بينما يبقى في المكان نفسه من شجرة React. تنشئ واحدة بـ createPortal(children, domNode) من react-dom. والبوابات هي طريقة إفلات النوافذ المنبثقة والتلميحات والقوائم المنسدلة من أب له overflow: hidden أو سياق تراكب خاص به.

يقصّ الصندوق أدناه كل ما يبرز منه. افتح التلميحين.

يُقطع التلميح العادي عند الحد المتقطع. أما تلميح البوابة فيظهر كاملًا، لأن عقدة DOM الخاصة به ابن لـ <body>، لا للصندوق. احذف overflow: 'hidden' من الصندوق فلا يعود التلميح العادي مقصوصًا.

الصيغة

import { createPortal } from 'react-dom';

createPortal(children, domNode, key?)
  • children أي JSX: عنصر، أو جزء، أو مكوّن.
  • domNode عنصر DOM موجود، مثل document.body أو document.getElementById('modal-root'). ويجب أن يكون موجودًا عندما تُعرض البوابة.
  • key اختياري، لحين تعرض قائمة من البوابات.

تُرجع createPortal شيئًا تضعه في JSX مثل أي عنصر. ولا تعرض شيئًا في موضع DOM الخاص بالأب.

نافذة منبثقة في document.body

النافذة المنبثقة هي الحالة الكلاسيكية. داخل بطاقة لها transform، يتحدد موضع الغطاء position: fixed نسبة إلى تلك البطاقة بدلًا من النافذة، ثم يقصّه overflow: hidden الخاص بالبطاقة. أما عند عرضه داخل document.body فيغطي منفذ العرض كله.

افتح النافذة المنبثقة، ثم اضغط Escape أو انقر الغطاء الداكن لإغلاقها. ينتقل التركيز إلى زر Close عندما تنفتح. أرجع الآن الغطاء دون createPortal (احذف الاستدعاء ووسيطه document.body): يتقلص الغطاء إلى حجم البطاقة، لأن transform يجعل البطاقة الكتلة الحاوية للعناصر الثابتة.

الأحداث تنتشر عبر شجرة React

تغيّر البوابة مكان عيش عقدة DOM، لا مكان عيش المكوّن. تنتشر أحداث React صعودًا عبر شجرة React، فتصل النقرة داخل بوابة إلى onClick الخاص بالمكوّن الذي عرضها، رغم أن الزر في DOM ابن لـ <body>.

أما المستمعات الأصلية فمختلفة. المستمع المضاف بـ addEventListener يتبع شجرة DOM ولا يرى النقرة أبدًا.

انقر "Inside the wrapper": يسجّل معالج React والمستمع الأصلي كلاهما. وانقر "In a portal": لا يسجّل إلا معالج React. يقع زر البوابة خارج الغلاف المتقطع على الشاشة، ومع ذلك ما زالت React تعامله كابن.

هذا عادة ما تريده: يرى onClick على أب القائمة النقرات على عناصر بوابتها، ومزوّدو السياق فوق المكوّن ينطبقون داخل البوابة أيضًا. لكنه قد يفاجئك مع منطق «انقر خارجها للإغلاق». فالـ onClick على غلاف يغلق قائمة يُطلق أيضًا للنقرات داخل بوابة القائمة، فأوقف الانتشار داخل القائمة (e.stopPropagation()، الذي تتناوله صفحة الأحداث)، أو استخدم مستمعًا أصليًا على document وتحقق مما إذا كان الهدف داخل عقدة DOM الخاصة بالبوابة.

بوابات داخل حاويتك الخاصة

document.body أبسط هدف. وتضيف بعض التطبيقات حاوية مخصصة إلى index.html لتتشارك كل الأغطية مكانًا واحدًا وترتيب تراكب واحدًا:

<body>
    <div id="root"></div>
    <div id="modal-root"></div>
</body>
createPortal(<Modal />, document.getElementById('modal-root'));

مع العرض على الخادم (Next.js وأطر العمل الأخرى)، لا يوجد document على الخادم. اعرض البوابة فقط بعد أن يُركَّب المكوّن، مثلًا خلف حالة mounted يضبطها تأثير على true.

وعندما يجب أن يتحدد موضع البوابة بجانب مشغّلها، قِس المشغّل أولًا. يقيسه المثال الأول في معالج النقر؛ أما للتلميح الذي يجب ألا يومض، فقِس في useLayoutEffect، الذي يعمل قبل أن يرسم المتصفح.

نوافذ منبثقة سهلة الوصول

نقل الترميز إلى document.body لا يفعل شيئًا بذاته لمستخدمي لوحة المفاتيح وقارئات الشاشة. تحتاج النافذة المنبثقة أيضًا إلى:

  • role="dialog" وaria-modal="true"، مع aria-labelledby يشير إلى عنوانها.
  • نقل التركيز إلى داخل الحوار عندما ينفتح (يركّز المثال على Close)، وإعادته إلى الزر الذي فتحه عندما ينغلق.
  • الإغلاق بـ Escape.
  • إبقاء التركيز داخله ما دام مفتوحًا، حتى لا يتجول Tab في الصفحة خلفه. ضبط السمة inert على جذر التطبيق أثناء فتح النافذة يمنع التركيز والنقرات هناك.

يتولى العنصر الأصلي <dialog> معظم هذا نيابة عنك. عند فتحه بـ dialogRef.current.showModal() يُرسم في الطبقة العليا للمتصفح فوق كل z-index، ويجعل باقي الصفحة خاملًا، وينغلق بـ Escape. لا يحتاج إلى بوابة، فهو خيار افتراضي جيد لحوارات التأكيد البسيطة؛ وتبقى البوابات الأداة المناسبة للتلميحات والقوائم والأغطية المخصصة.

لماذا لا يكفي z-index وحده

كثيرًا ما يلجأ المطورون إلى بوابة بعد أن يفشل z-index كبير في وضع قائمة فوق باقي الصفحة. والسبب سياقات التراكب. العنصر الذي له position وz-index، أو opacity أقل من 1، أو transform، أو filter، أو isolation: isolate يبدأ سياق تراكب جديدًا، ولا تتنافس قيم z-index لأبنائه إلا فيما بينها داخله. فالابن الذي له z-index: 9999 داخل بطاقة لها z-index: 1 ما زال يقع أسفل بطاقة شقيقة لها z-index: 2.

تنقل البوابة العنصر خارج كل سلف كهذا. وبصفته ابنًا لـ <body>، تُقارن قيمة z-index الخاصة به بعناصر المستوى الأعلى في الصفحة، فتكفي قيمة متواضعة مثل 1000 للأغطية.

أخطاء شائعة

إنشاء بوابة داخل عقدة لم توجد بعد. يُرجع document.getElementById('modal-root') القيمة null إذا كان العنصر مفقودًا، فترمي createPortal الخطأ "Target container is not a DOM element". تحقق من HTML، أو أنشئ البوابة داخل document.body.

إنشاء العقدة الهدف أثناء العرض. كتابة document.createElement('div') في جسم المكوّن تصنع عقدة جديدة في كل عرض. أنشئها مرة واحدة في تأثير، أو استخدم حاوية ثابتة.

نوافذ منبثقة لا تتبع مشغّلها. التلميح الذي يتحدد موضعه من getBoundingClientRect() لا يتبع مشغّله عندما تُمرَّر الصفحة أو يتغير حجمها. أعد الحساب عند scroll وresize (وأزل هذين المستمعين في دالة تنظيف التأثير)، أو أغلق التلميح عندما تُمرَّر الصفحة.

الأسئلة الشائعة

ما هي البوابة (portal) في React؟

طريقة لعرض الأبناء داخل عقدة DOM خارج عنصر DOM الخاص بالمكوّن الأب. تنشئ واحدة بـ createPortal(children, domNode) من react-dom. ويحتفظ الأبناء بمكانهم في شجرة React، فتعمل props والحالة والسياق كالمعتاد.

متى أستخدم بوابة؟

عندما يجب أن يظهر شيء فوق حاويته أو خارجها: النوافذ المنبثقة، والتلميحات، والقوائم المنسدلة، والإشعارات. فالأب الذي له overflow: hidden أو transform أو سياق تراكب خاص به سيقصّه أو يخفيه لولا ذلك.

هل تنتشر الأحداث خارج البوابة؟

نعم، عبر شجرة React. النقرة داخل بوابة تصل إلى معالجات onClick على آباء React، رغم أن عقدة DOM تعيش في document.body. أما المستمعات الأصلية المضافة بـ addEventListener فتتبع شجرة DOM بدلًا من ذلك.

هل يعمل السياق داخل بوابة؟

نعم. السياق، مثل الأحداث، يتبع شجرة React. النافذة المنبثقة المعروضة داخل document.body ما زالت تقرأ السمة أو المستخدم من المزوّدين فوق المكوّن الذي أنشأها.

هل أحتاج إلى بوابة لنافذة منبثقة؟

ليس دائمًا. العنصر الأصلي <dialog> المفتوح بـ showModal() يُرسم في الطبقة العليا للمتصفح، فوق كل z-index، مع معالجة مدمجة للتركيز. أما البوابة فهي الخيار المعتاد للنوافذ المنبثقة المخصصة وللتلميحات والقوائم.

رسم توضيحي للغات البرمجة في Coddy

تعلّم البرمجة مع Coddy

ابدأ الآن