Portals — modal, tooltip uchun, DOM dan tashqariga chiqish

~15 daq · React · Ilg'or mavzular

Muammodan boshlaylik

Mahsulot kartochkasida "O'chirish" tugmasi tasdiqlash modalini ochadi:

function MahsulotKartochka({ mahsulot }) {
  const [ochiq, setOchiq] = useState(false);
  return (
    <article className="kartochka">
      <h3>{mahsulot.nomi}</h3>
      <button onClick={() => setOchiq(true)}>O'chirish</button>
      {ochiq && <Modal onYopish={() => setOchiq(false)}>Ishonchingiz komilmi?</Modal>}
    </article>
  );
}
.modal-fon {
  position: fixed;
  inset: 0;
  z-index: 1000;
}

Kutilgan: butun ekranni qoplovchi modal. Haqiqatda:

  • Modal kartochka ichida qisilib qoldi — kartochkada overflow: hidden bor
  • z-index: 1000 ga qaramay, modal qo'shni kartochka ostida — kartochkada transform bor va u yangi "stacking context" yaratgan (CSS kursi, 11-dars)
  • position: fixed ishlamayapti — ota-onadagi transform uni ekranga emas, kartochkaga nisbatan joylashtiradi

Muammo CSS da emas, DOM dagi joylashuvda: modal kartochkaning ichida, va ota-onaning stillari unga ta'sir qiladi.


createPortal

Portal — komponentni React daraxtidagi joyida qoldirib, uni DOM ning boshqa joyiga render qilish:

import { createPortal } from 'react-dom';

function Modal({ children, onYopish }) {
  return createPortal(
    <div className="modal-fon" onClick={onYopish}>
      <div className="modal" onClick={(e) => e.stopPropagation()}>
        {children}
        <button onClick={onYopish}>Yopish</button>
      </div>
    </div>,
    document.body                       // ← shu yerga render qilinadi
  );
}

createPortal(jsx, domElement):

  • jsx — nima render qilinadi
  • domElement — DOM ning qayeriga
React daraxti:                       DOM:
App                                  <body>
└── MahsulotKartochka                  <div id="root">
    └── Modal  ─────────────┐            <article class="kartochka">...</article>
                            │          </div>
                            └────────▶ <div class="modal-fon">...</div>   ← body ning oxirida
                                     </body>

Endi modal body ning to'g'ridan-to'g'ri bolasi: ota-onaning overflow, transform va z-index i unga ta'sir qilmaydi.

Alohida konteyner

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

Barcha modal, tooltip va bildirishnomalar bitta joyda — DevTools da topish oson.


Portal — React uchun oddiy bola

DOM da modal boshqa joyda, lekin React uchun u hali ham MahsulotKartochka ning bolasi:

1. Context ishlaydi

<ThemeContext.Provider value="qorongi">
  <MahsulotKartochka />          {/* ichidagi Modal ham "qorongi" ni oladi */}
</ThemeContext.Provider>

2. Props va state — odatdagidek

Modal mahsulot ni props orqali oladi, setOchiq bilan yopiladi — hech qanday farq yo'q.

3. Hodisalar React daraxti bo'ylab ko'tariladi

<div onClick={() => console.log('Kartochka bosildi')}>
  <Modal>...</Modal>             {/* modal ichidagi bosish ham bu yerga yetib keladi! */}
</div>

DOM da modal kartochka ichida emas, lekin React hodisalari (onClick, onKeyDown) React daraxti bo'yicha ko'tariladi (bubbling). Modal ichidagi bosish kartochkaning onClick ini ham ishga tushiradi.

[!WARNING] Bu kutilmagan xatolarga olib kelishi mumkin: kartochka butunligicha havola bo'lsa (onClick={() => navigate(...)}), modal ichidagi istalgan bosish sahifani almashtiradi. Kerak bo'lsa modal ildizida onClick={(e) => e.stopPropagation()} qiling.


To'liq modal — accessibility bilan

Portal faqat joylashuv muammosini hal qiladi. Haqiqiy modal uchun yana kerak (HTML kursi, 21–22-darslar): fokusni ichkarida ushlash, Esc bilan yopish, yopilganda fokusni qaytarish, orqadagi sahifani bloklash.

Eng oson yo'l — HTML ning <dialog> elementi:

import { useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';

function Modal({ ochiq, onYopish, sarlavha, children }) {
  const ref = useRef(null);

  useEffect(() => {
    const dialog = ref.current;
    if (ochiq && !dialog.open) dialog.showModal();
    if (!ochiq && dialog.open) dialog.close();
  }, [ochiq]);

  return createPortal(
    <dialog
      ref={ref}
      onClose={onYopish}                       // Esc bosilganda ham
      aria-labelledby="modal-sarlavha"
      className="modal"
    >
      <h2 id="modal-sarlavha">{sarlavha}</h2>
      {children}
      <button onClick={onYopish}>Yopish</button>
    </dialog>,
    document.body
  );
}

showModal() o'zi beradi: fokus ichkarida, orqa fon inert, Esc bilan yopish, yopilganda fokus qaytadi, ::backdrop bilan fon (HTML kursi, 22-dars). <dialog> "top layer" da ko'rsatiladi — z-index muammolari umuman yo'q.

[!INFO] <dialog> top layer ga chiqqani uchun z-index muammosini portal siz ham hal qiladi. Portal baribir foydali: modal DOM da sahifa kontentidan ajralgan bo'ladi, va ota-onadagi CSS (masalan, display: none yoki content-visibility) unga ta'sir qilmaydi.


Tooltip va dropdown

function Tooltip({ matn, children }) {
  const [joy, setJoy] = useState(null);
  const ref = useRef(null);

  function korsat() {
    const r = ref.current.getBoundingClientRect();
    setJoy({ top: r.bottom + 8, left: r.left + r.width / 2 });
  }

  return (
    <>
      <span
        ref={ref}
        onMouseEnter={korsat}
        onMouseLeave={() => setJoy(null)}
        onFocus={korsat}
        onBlur={() => setJoy(null)}
        aria-describedby={joy ? 'tooltip' : undefined}
      >
        {children}
      </span>
      {joy &&
        createPortal(
          <div
            id="tooltip"
            role="tooltip"
            className="tooltip"
            style={{ position: 'fixed', top: joy.top, left: joy.left, translate: '-50% 0' }}
          >
            {matn}
          </div>,
          document.body
        )}
    </>
  );
}

Tooltip overflow: hidden li jadval yoki scroll konteyner ichida bo'lsa ham — to'liq ko'rinadi.

[!TIP] Tooltip va dropdown larning joylashuvi murakkab: ekran chetiga yaqinlashganda tomonni almashtirish, scroll da kuzatish, o'lchamlarni hisoblash. Buning uchun Floating UI kutubxonasi bor; Radix UI, Headless UI kabi komponent kutubxonalari esa portal, joylashuv va accessibility ni tayyor holda beradi. Shuningdek, brauzerlarda yangi Popover API (popover atributi) va CSS anchor positioning paydo bo'lmoqda.


Qachon portal kerak

✅ Portal ❌ Kerak emas
Modal va dialog oynalar Oddiy ochiladigan bloklar (akkordeon)
Tooltip, popover, dropdown menyular (overflow li konteynerda) Element o'z joyida to'g'ri ko'rinsa
Toast bildirishnomalar
Sahifadan tashqaridagi DOM ga render (masalan, boshqa oyna, iframe)

Nima uchun shunday?

Nima uchun portal dagi hodisalar DOM bo'yicha emas, React daraxti bo'yicha ko'tariladi?

Chunki bu komponentlarni inkapsulyatsiya qiladi. Modal komponenti MahsulotKartochka ning ichki qismi — u qayerga render qilinishi uning implementatsiya tafsiloti. Agar hodisalar DOM bo'yicha ko'tarilsa, portal ishlatish yoki ishlatmaslik komponentning xatti-harakatini o'zgartirardi: bir kun createPortal qo'shsangiz, ota-onadagi onKeyDown yoki onClick lar jimgina ishlamay qo'yardi. React izchil qoida tanlagan: hamma narsa — Context, props, hodisalar — React daraxti bo'yicha ishlaydi. DOM dagi joylashuv esa faqat ko'rinish masalasi. Bu sizga komponentni ichini o'zgartirmasdan uni ota-onaning CSS cheklovlaridan "qochirish" imkonini beradi.


Xulosa

Eslab qoling:

  • Muammo: ota-onadagi overflow: hidden, transform, z-index modal va tooltip larni buzadi
  • createPortal(jsx, domElement) — React daraxtida joyida, DOM da boshqa joyda
  • Odatda document.body yoki alohida #modal-root
  • React uchun portal — oddiy bola: Context, props, state ishlaydi
  • Hodisalar React daraxti bo'ylab ko'tariladi — kerak bo'lsa stopPropagation
  • Modal uchun — portal + <dialog> + showModal(): fokus, Esc, inert bepul
  • Tooltip/dropdown joylashuvi uchun — Floating UI yoki tayyor komponent kutubxonalari

Keyingi dars

81-dars: Suspense — data fetching, skeleton UI