ポータル(Portal)を利用することでモーダルやトーストのような『画面上に飛び出すUIパーツ』を実装できます。
今回はcreatePortal
を利用したポータルの実装方法について紹介します。
検証環境はnext 12.1.4
、react 18.0.0
です。
目次
createPortalについて
createPortal
はポータルを作成するメソッドです。
createPortal
は引数を2つ持ちます。第1引数(child
)にはReactの子要素としてレンダー可能な要素、第2引数(container
)にはDOM要素を指定します。
下準備
ポータルの検証で利用するコンポーネント(以下でいうExample
)を呼び出すPageを作成しておきます。
src/pages/index.tsx
import type { NextPage } from "next";
import { Example } from "../components/Example";
const Home: NextPage = () => {
return <Example />;
};
export default Home;
createPortalの第1引数(child)の指定パターン
コンポーネントを利用する場合
src/components/Example.tsx
import { FC } from "react";
import { ExamplePortal } from "src/components/shared/ExamplePortal";
export const Example: FC = () => {
return (
<>
<div>Example</div>
<ExamplePortal />
</>
);
};
src/components/shared/ExamplePortal.tsx
import { FC, useEffect, useState } from "react";
import { createPortal } from "react-dom";
export const ExamplePortal: FC = () => {
const [showPortal, setShowPortal] = useState(false);
useEffect(() => {
setShowPortal(true);
}, []);
if (!showPortal) {
return null;
}
return createPortal(
<h1>Hello Portals</h1>,
document.getElementById("__next")! // __next: Next.jsのroot divに適用されているid
);
};
ローカルState(showPortal
)を導入してSSRでWindowを参照しないようにしています。
詳細は【Next.js】SSR/SGでブラウザ機能(Cookieなど)を活用する際の注意点で紹介しています。
なお、document.getElementById("__next")!
の!
はTypeScriptのNon-null assertion operatorです。
コンポジション(props.children)を利用する場合
第1引数にはコンポジションの指定もできます。
src/components/Example.tsx
import { FC } from "react";
import { ExamplePortal } from "src/components/shared/ExamplePortal";
export const Example: FC = () => {
return (
<>
<div>Example</div>
<ExamplePortal>
<h1>Hello Portals</h1>
</ExamplePortal>
</>
);
};
src/components/shared/ExamplePortal.tsx
import { FC, ReactNode, useEffect, useState } from "react";
import { createPortal } from "react-dom";
type Props = {
children: ReactNode;
};
export const ExamplePortal: FC<Props> = ({ children }) => {
const [showPortal, setshowPortal] = useState(false);
useEffect(() => {
setshowPortal(true);
}, []);
if (!showPortal) {
return null;
}
return createPortal(children, document.getElementById("__next")!);
};
createPortalを適用するDOMの準備方法
方法1: 既存のDOMを利用する
アプリケーションですでに存在しているDOM要素にポータルを適用する方法です。
上記で紹介した、__next
というidが付与されたNext.jsのデフォルトのroot divを利用する実装もこの方法に該当します。
サンプルコードは省略します。
方法2: ポータルを適用するDOMを作成する
ポータル用の要素とidをJSXに記述する方法です。
たとえばindex.tsx
でポータルを表示するdiv要素を作成します。
src/pages/index.tsx
import type { NextPage } from "next";
import { Example } from "../components/Example";
const Home: NextPage = () => {
return (
<>
<Example />
<div id="portal"></div> {/* ここにポータルが描画される */}
</>
);
};
export default Home;
次に、以下のようにして作成した要素に対してポータルを適用します。
createPortal(children, document.getElementById("portal")!);
方法3: createPortalを実行する際にDOMの生成を行う
createPoratlでポータルを描画する際に描画用のdiv要素を作成する方法です。
以下はid
がportal
のdiv要素を作成する例です。
src/components/shared/ExamplePortal.tsx
import { FC, useEffect, useState } from "react";
import { createPortal } from "react-dom";
const createElement = (): HTMLElement => {
const el = document.createElement("div");
el.setAttribute("id", "portal");
return el;
};
export const ExamplePortal: FC = () => {
const [showPortal, setShowPortal] = useState(false);
useEffect(() => {
// portalというidが付与されたdiv要素を探し、なければ作成。
const el =
document.querySelector<HTMLElement>("#portal") ?? createElement();
// <div id="portal">をbodyの子要素に追加
document.body.appendChild(el);
}, []);
useEffect(() => {
setShowPortal(true);
}, []);
if (!showPortal) {
return null;
}
return createPortal(
<h1>Hello Portals</h1>,
document.getElementById("portal")!
);
};
この方法を利用するとJSX側で<div id="portal"></div>
のようにしてポータル用の要素を用意しなくてよくなります。
なお、document.querySelector<HTMLElement>("#portal") ?? createElement();
の??
はNull合体演算子(Nullish coalescing operator)です。
Null合体演算子の詳細解説は【JavaScript】Null合体(Nullish coalescing)とオプショナルチェーン(Optional chaining)の基本で紹介しています。
参考: カスタムフックを利用したリファクタリング例
div要素を作成する副作用をカスタムフックとして切り出すと以下のようになります。
src/lib/hooks/usePortal.ts
import { useEffect, useState } from "react";
const createElement = (): HTMLElement => {
const el = document.createElement("div");
el.setAttribute("id", "portal");
return el;
};
export const usePortal = () => {
const [element, setElement] = useState<HTMLElement | null>(null);
useEffect(() => {
const el =
document.querySelector<HTMLElement>("#portal") ?? createElement();
document.body.appendChild(el);
setElement(el);
}, []);
return element;
};
src/components/shared/ExamplePortal.tsx
import { FC, useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { usePortal } from "src/lib/hooks/usePortal";
export const ExamplePortal: FC = () => {
const [showPortal, setShowPortal] = useState(false);
const portal = usePortal();
useEffect(() => {
setShowPortal(true);
}, []);
if (!showPortal) {
return null;
};
return createPortal(<h1>Hello Portals</h1>, portal!);
};
カスタムフックの詳細解説は【React】カスタムフックの概要・メリット・使いどころで紹介しています。
さいごに
createPortalを利用したモーダル実装の詳細は【React】createPortalとContextによるモーダル実装で、トースト実装の詳細は【React】createPortalとContextによるトースト(Toast)実装で紹介しています。
Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!