木瓜丸です。
Mantineとか、ChakraUIとか、UIライブラリはいいよなぁ。
でも痒い所に手が届かないことがあったり、既存のものを改造しているとそれだけで日が暮れたりすることがあるので、一般的じゃないUIを作る時はこういうものを使わないで全部自前で作ったりします。
この度もCSSを自分で書いて個人開発しているのですが、そんな中でMantineのMenuコンポーネントのようなものはどのように実装すればいいのかなと思い立ち、createPortal
というやつを発見しました。
この記事では、createPortal
の使い方を簡単にまとめてみたいと思います。
MantineのMenu.Dropdownは親子関係をガン無視する
まずは、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が別の要素内に展開されます。
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とも相性が良さそうですね。