React / Portal

Portal

React-д component зөвхөн өөрийн parent-н DOM дотор рендерлэгддэг. Гэхдээ заримдаа — modal цонх, tooltip, dropdown гэх мэт — component-ийг DOM-н өөр хэсэгт рендерлэх шаардлага гардаг. Portal яг үүнд зориулагдсан!

Яагаад Portal хэрэгтэй вэ?

CSS-н overflow: hidden эсвэл z-index асуудлаас болж modal цонх буруу харагдах тохиолдол байдаг:

jsx
// ❌ Асуудлыг харцгаая
function Card() {
  const [open, setOpen] = useState(false);

  return (
    // overflow: hidden нь modal-г "тайрдаг"
    <div
      style={{ overflow: "hidden", height: "100px", border: "1px solid red" }}
    >
      <button onClick={() => setOpen(true)}>Modal нээх</button>
      {open && (
        // Энэ modal Card-н overflow-д баригдаж харагдахгүй болно!
        <div
          style={{
            position: "fixed",
            top: "50%",
            left: "50%",
            transform: "translate(-50%, -50%)",
            background: "white",
            padding: "20px",
            zIndex: 1000,
          }}
        >
          <p>Modal агуулга</p>
          <button onClick={() => setOpen(false)}>Хаах</button>
        </div>
      )}
    </div>
  );
}

Portal ашиглан modal-г document.body-д шууд рендерлэвэл энэ асуудал арилна.

createPortal ашиглах нь

react-dom-аас createPortal import хийнэ:

jsx
import { useState } from "react";
import { createPortal } from "react-dom";

// Modal component — document.body дотор рендерлэгдэнэ
function Modal({ children, onClose }) {
  return createPortal(
    // Рендерлэх JSX
    <div
      style={{
        position: "fixed",
        inset: 0,
        background: "rgba(0,0,0,0.5)",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        zIndex: 9999,
      }}
      onClick={onClose}
    >
      <div
        style={{ background: "white", padding: "24px", borderRadius: "8px" }}
        onClick={(e) => e.stopPropagation()}
      >
        {children}
        <button onClick={onClose}>Хаах ✕</button>
      </div>
    </div>,
    // DOM-н аль element-д оруулах вэ?
    document.body,
  );
}

export default function App() {
  const [open, setOpen] = useState(false);

  return (
    <div
      style={{ overflow: "hidden", height: "100px", border: "1px solid #ccc" }}
    >
      <button onClick={() => setOpen(true)}>Modal нээх</button>
      {open && (
        <Modal onClose={() => setOpen(false)}>
          <h2>Та итгэлтэй байна уу?</h2>
          <p>Энэ үйлдлийг буцааж болохгүй.</p>
        </Modal>
      )}
    </div>
  );
}

createPortal хоёр аргумент авдаг:

  1. JSX — рендерлэх агуулга
  2. DOM element — хаана рендерлэх вэ (ихэвчлэн document.body)

Бүрэн modal жишээ

Практикт хэрэглэж болох modal component:

jsx
import { useEffect } from "react";
import { createPortal } from "react-dom";

function Modal({ title, children, onClose }) {
  // Escape товч дарахад хаах
  useEffect(() => {
    function handleKey(e) {
      if (e.key === "Escape") onClose();
    }
    document.addEventListener("keydown", handleKey);
    // Modal нээлттэй байх үед scroll хаах
    document.body.style.overflow = "hidden";

    return () => {
      document.removeEventListener("keydown", handleKey);
      document.body.style.overflow = "";
    };
  }, [onClose]);

  return createPortal(
    <div
      role="dialog"
      aria-modal="true"
      style={{
        position: "fixed",
        inset: 0,
        background: "rgba(0,0,0,0.6)",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        zIndex: 9999,
      }}
    >
      <div
        style={{
          background: "#fff",
          borderRadius: "12px",
          padding: "24px",
          maxWidth: "480px",
          width: "90%",
        }}
      >
        <div style={{ display: "flex", justifyContent: "space-between" }}>
          <h2 style={{ margin: 0 }}>{title}</h2>
          <button onClick={onClose} aria-label="Хаах"></button>
        </div>
        <div style={{ marginTop: "16px" }}>{children}</div>
      </div>
    </div>,
    document.body,
  );
}

// Хэрэглэх жишээ
export default function App() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <button onClick={() => setOpen(true)}>Мэдээлэл харах</button>
      {open && (
        <Modal title="Дансны мэдээлэл" onClose={() => setOpen(false)}>
          <p>Таны данс амжилттай нэмэгдлээ.</p>
          <button onClick={() => setOpen(false)}>Хаах</button>
        </Modal>
      )}
    </>
  );
}

Portal-н давуу тал

Portal ашигласнаар:

  • overflow: hidden болон z-index асуудлуудаас ангижирна
  • Event bubbling хэвийн ажилладаг — Portal-н event нь React-н component tree-г дагаж bubbles хийдэг (DOM tree-г биш)
  • Modal, tooltip, dropdown, notification зэрэг UI элементийг аюулгүй зохион байгуулах боломжтой
jsx
// Event bubbling жишээ — Portal дотор дарахад Parent мэдэрнэ
function Parent() {
  return (
    <div onClick={() => console.log("Parent мэдэрлээ!")}>
      <Modal ...>
        <button>Дарах</button> {/* Энд дарахад Parent log хэвлэнэ */}
      </Modal>
    </div>
  );
}

Дараагийн хичээлд:

Error Boundary сурна — component-д алдаа гарахад аппыг бүхэлд нь унагааж дуусахаас хамгаалах механизм. Хэрэглэгчид тааламжтай алдааны мэдэгдэл харуулах аргыг эзэмшинэ.