Menu

Portals ב-React: createPortal למודאלים ול-tooltips

createPortal מרנדר חלק מקומפוננטה לתוך צומת DOM אחר, כמו document.body, בזמן שהוא נשאר באותו מקום בעץ של React. השתמשו בו למודאלים, ל-tooltips ולתפריטים שצריכים לברוח מ-overflow hidden ומשכבות z-index.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

portal מרנדר חלק מקומפוננטה למקום אחר ב-DOM, בדרך כלל document.body, בזמן שהוא נשאר באותו מקום בעץ של React. יוצרים אחד עם createPortal(children, domNode) מ-react-dom. portals הם הדרך שבה מודאלים, tooltips ותפריטים נפתחים בורחים מהורה שיש לו overflow: hidden או stacking context משלו.

התיבה שמתחת חותכת כל דבר שבולט ממנה. פתחו את שני הטיפים.

הטיפ הרגיל נחתך בגבול המקווקו. הטיפ ב-portal מופיע במלואו, כי צומת ה-DOM שלו הוא ילד של <body>, לא של התיבה. מחקו את overflow: 'hidden' מהתיבה והטיפ הרגיל כבר לא נחתך.

התחביר

import { createPortal } from 'react-dom';

createPortal(children, domNode, key?)
  • children הוא כל JSX: אלמנט, fragment, קומפוננטה.
  • domNode הוא אלמנט DOM קיים, כמו document.body או document.getElementById('modal-root'). הוא חייב להתקיים כשה-portal מתרנדר.
  • key אופציונלי, למקרה שמרנדרים רשימה של portals.

createPortal מחזיר משהו ששמים ב-JSX כמו כל אלמנט. הוא לא מרנדר כלום במיקום ה-DOM של ההורה עצמו.

מודאל ב-document.body

מודאל הוא המקרה הקלאסי. בתוך כרטיס עם transform, שכבת overlay עם position: fixed ממוקמת ביחס לכרטיס הזה במקום לחלון, וה-overflow: hidden של הכרטיס חותך אותה. כשהיא מרונדרת לתוך document.body, היא מכסה את כל ה-viewport.

פתחו את המודאל, ואז לחצו Escape או על ה-overlay הכהה כדי לסגור אותו. הפוקוס עובר לכפתור ה-Close כשהוא נפתח. עכשיו החזירו את ה-overlay בלי createPortal (הסירו את הקריאה ואת הארגומנט document.body שלה): ה-overlay מתכווץ לגודל הכרטיס, כי transform הופך את הכרטיס ל-containing block של אלמנטים fixed.

אירועים עולים דרך העץ של React

portal משנה איפה צומת ה-DOM חי, לא איפה הקומפוננטה חיה. אירועי React עולים במעלה העץ של React, כך שלחיצה בתוך portal מגיעה ל-onClick של הקומפוננטה שרינדרה אותו, למרות שב-DOM הכפתור הוא ילד של <body>.

listeners נייטיב שונים. listener שנוסף עם addEventListener עוקב אחרי עץ ה-DOM ואף פעם לא רואה את הלחיצה.

לחצו על "Inside the wrapper": גם ה-handler של React וגם ה-listener הנייטיב רושמים. לחצו על "In a portal": רק ה-handler של React רושם. כפתור ה-portal יושב מחוץ לעטיפה עם הגבול המקווקו על המסך, ובכל זאת React עדיין מתייחסת אליו כאל ילד.

לרוב זה מה שרוצים: onClick על ההורה של תפריט רואה לחיצות על פריטי ה-portal שלו, ו-providers של קונטקסט מעל הקומפוננטה חלים גם בתוך ה-portal. זה יכול להפתיע עם לוגיקה של "לחיצה בחוץ סוגרת". onClick על עטיפה שסוגרת תפריט מופעל גם בלחיצות בתוך ה-portal של התפריט, אז עצרו את ההתפשטות בתוך התפריט (e.stopPropagation(), שמכוסה בדף על אירועים), או השתמשו ב-listener נייטיב על document ובדקו אם היעד נמצא בתוך צומת ה-DOM של ה-portal.

portals לתוך מכל משלכם

document.body הוא היעד הפשוט ביותר. חלק מהאפליקציות מוסיפות מכל ייעודי ל-index.html כדי שכל שכבות ה-overlay יחלקו מקום אחד וסדר שכבות אחד:

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

עם רינדור בצד השרת (Next.js ו-frameworks אחרים), document לא קיים בשרת. רנדרו את ה-portal רק אחרי שהקומפוננטה עשתה mount, למשל מאחורי state mounted שאפקט קובע ל-true.

כשה-portal צריך להיות ממוקם ליד הכפתור שמפעיל אותו, מדדו קודם את הכפתור. הדוגמה הראשונה מודדת אותו ב-handler של הלחיצה; ל-tooltip שאסור לו להבהב, מדדו ב-useLayoutEffect, שרץ לפני שהדפדפן מצייר.

מודאלים נגישים

העברת ה-markup ל-document.body לא עושה כלום בפני עצמה בשביל משתמשי מקלדת וקוראי מסך. מודאל צריך גם:

  • role="dialog" ו-aria-modal="true", עם aria-labelledby שמצביע על הכותרת שלו.
  • פוקוס שעובר לתוך הדיאלוג כשהוא נפתח (הדוגמה נותנת פוקוס ל-Close), וחוזר לכפתור שפתח אותו כשהוא נסגר.
  • Escape לסגירה.
  • פוקוס שנשאר בפנים כל עוד הוא פתוח, כך ש-Tab לא מטייל לדף שמאחור. קביעת המאפיין inert על ה-root של האפליקציה בזמן שהמודאל פתוח חוסמת שם פוקוס ולחיצות.

האלמנט הנייטיב <dialog> מטפל ברוב זה בשבילכם. כשהוא נפתח עם dialogRef.current.showModal(), הוא מצויר ב-top layer של הדפדפן מעל כל z-index, הופך את שאר הדף ל-inert, ונסגר ב-Escape. הוא לא צריך portal, ולכן הוא ברירת מחדל טובה לדיאלוגים פשוטים של אישור; portals נשארים הכלי ל-tooltips, לתפריטים ולשכבות overlay מותאמות.

למה z-index לבד לא מספיק

מפתחים פונים לעיתים קרובות ל-portal אחרי ש-z-index גדול לא מצליח לשים תפריט מעל שאר הדף. הסיבה היא stacking contexts. אלמנט עם position ו-z-index, עם opacity מתחת ל-1, עם transform, עם filter או עם isolation: isolate מתחיל stacking context חדש, וערכי ה-z-index של הילדים שלו מתחרים רק זה בזה בתוכו. ילד עם z-index: 9999 בתוך כרטיס עם z-index: 1 עדיין יושב מתחת לכרטיס אח עם z-index: 2.

portal מוציא את האלמנט מכל אב כזה. כילד של <body>, ה-z-index שלו מושווה לאלמנטים ברמה העליונה של הדף, כך שערך צנוע כמו 1000 מספיק לשכבות overlay.

טעויות נפוצות

portal לתוך צומת שעוד לא קיים. document.getElementById('modal-root') מחזיר null אם האלמנט חסר, ו-createPortal זורק "Target container is not a DOM element". בדקו את ה-HTML, או עשו portal לתוך document.body.

יצירת צומת היעד בזמן הרינדור. כתיבת document.createElement('div') בגוף הקומפוננטה יוצרת צומת חדש בכל רינדור. צרו אותו פעם אחת באפקט, או השתמשו במכל קבוע.

חלונות קופצים שלא עוקבים אחרי הכפתור שלהם. tooltip שממוקם מ-getBoundingClientRect() לא עוקב אחרי הכפתור שלו כשהדף נגלל או משנה גודל. חשבו מחדש ב-scroll וב-resize (והסירו את ה-listeners האלה ב-cleanup של האפקט), או סגרו את ה-tooltip כשהדף נגלל.

שאלות נפוצות

מה זה portal ב-React?

דרך לרנדר ילדים לתוך צומת DOM מחוץ לאלמנט ה-DOM של קומפוננטת ההורה. יוצרים אחד עם createPortal(children, domNode) מ-react-dom. הילדים שומרים על המקום שלהם בעץ של React, כך ש-props, state וקונטקסט עובדים כרגיל.

מתי להשתמש ב-portal?

כשמשהו צריך להופיע מעל המכל שלו או מחוצה לו: מודאלים, tooltips, תפריטים נפתחים, toasts. הורה עם overflow: hidden, עם transform או עם stacking context משלו היה חותך או מסתיר אותו אחרת.

האם אירועים עולים (bubble) מתוך portal?

כן, דרך העץ של React. לחיצה בתוך portal מגיעה ל-handlers של onClick על ההורים ב-React, למרות שצומת ה-DOM חי ב-document.body. listeners נייטיב שנוספו עם addEventListener עוקבים במקום זאת אחרי עץ ה-DOM.

האם קונטקסט עובד בתוך portal?

כן. קונטקסט, כמו אירועים, עוקב אחרי העץ של React. מודאל שמרונדר לתוך document.body עדיין קורא את ערכת הנושא או את המשתמש מה-providers שמעל הקומפוננטה שיצרה אותו.

צריך portal בשביל מודאל?

לא תמיד. האלמנט הנייטיב <dialog> שנפתח עם showModal() מצויר ב-top layer של הדפדפן, מעל כל z-index, עם טיפול בפוקוס מובנה. portal הוא הבחירה הרגילה למודאלים מותאמים ול-tooltips ולתפריטים.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל