記事一覧へ
8/25/2023

react-domのcreatePortalで要素の親子関係をガン無視する

木瓜丸です。

Mantineとか、ChakraUIとか、UIライブラリはいいよなぁ。

でも痒い所に手が届かないことがあったり、既存のものを改造しているとそれだけで日が暮れたりすることがあるので、一般的じゃないUIを作る時はこういうものを使わないで全部自前で作ったりします。

この度もCSSを自分で書いて個人開発しているのですが、そんな中でMantineのMenuコンポーネントのようなものはどのように実装すればいいのかなと思い立ち、createPortalというやつを発見しました。

この記事では、createPortalの使い方を簡単にまとめてみたいと思います。

MantineのMenu.Dropdownは親子関係をガン無視する

まずは、MantineのMenuコンポーネントについて紹介します。

MantineのMenuコンポーネント

import { Menu, Button, Text } from '@mantine/core';
import { IconSettings, IconSearch, IconPhoto, IconMessageCircle, IconTrash, IconArrowsLeftRight } from '@tabler/icons-react';

function Demo() {
  return (
    <Menu shadow="md" width={200}>
      <Menu.Target>
        <Button>Toggle menu</Button>
      </Menu.Target>

      <Menu.Dropdown>
        <Menu.Label>Application</Menu.Label>
        <Menu.Item icon={<IconSettings size={14} />}>Settings</Menu.Item>
        <Menu.Item icon={<IconMessageCircle size={14} />}>Messages</Menu.Item>
        <Menu.Item icon={<IconPhoto size={14} />}>Gallery</Menu.Item>
        <Menu.Item
          icon={<IconSearch size={14} />}
          rightSection={<Text size="xs" color="dimmed">⌘K</Text>}
        >
          Search
        </Menu.Item>

        <Menu.Divider />

        <Menu.Label>Danger zone</Menu.Label>
        <Menu.Item icon={<IconArrowsLeftRight size={14} />}>Transfer my data</Menu.Item>
        <Menu.Item color="red" icon={<IconTrash size={14} />}>Delete my account</Menu.Item>
      </Menu.Dropdown>
    </Menu>
  );
}

(引用元: Menu | Mantine)

Menu.Target内部の要素をクリックすると、Menu.Dropdownの内容が表示されます。 一見シンプルですが、この要素はoverflowに対応するためか、Menu.Dropdownが別の要素内に展開されます。

Menu.Dropdownが別の親の中に展開されている

createPortalとは

createPortalを使うと、指定したDOMオブジェクト下にコンポーネントを展開することができます。

使い方

react-domからcreatePortalをimportして用います。

まずは、指定のDOMに対してchildrenを展開するPortalコンポーネントを作成します。

import { createPortal } from 'react-dom';

const Portal = ({ children }) => {
  const target = document.querySelector("#portal")
  return createPortal(children, target)
}

export default Portal

要素を配置する側のコンポーネントからは次のようにPortal配下にコンポーネントを配置します。 これにより、#portal配下に要素が展開されます。

import Portal from 'path/to/Portal'

const Container = () => {
  const [isOpen, setIsOpen] = useState(false)
  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>Open Menu</button>
      {isOpen && (
        <Portal>
          <div>
            <button onClick={() => setIsOpen(false)}>Close</button>
          </div>
        </Portal>
      )}
    </div>
  )
}

export default Container

まとめ

特にz-indexやoverflowが絡んでくる場合には親子関係が悩みの種になりがちですが、createPortalを有効活用できると開発の幅を広げられそうだなと思いました。 Contextとも相性が良さそうですね。


書いた人

木瓜丸

Webエンジニア。2022年に「木瓜丸屋」を開業し、個人開発をしています。

その他プロフィールをチェック