【React】createPortalを使ったコンポーネント作成
こんにちは!
スペースマーケットでフロントエンドエンジニアをしているwharaguchiです。
先日createPortal APIを使用してコンポーネントを作成しましたので、今回はそちらについて書きたいと思います。
createPortalとは
createPortalとは、Reactが提供しているAPIで、DOM上の任意の場所に要素をレンダーすることができるAPIです。
サンプルコードを公式ドキュメントからそのまま引用させてもらいますが、以下のように第一引数にレンダーしたもの、第二引数にレンダー先に指定して使用します。
また第三引数に一意のkeyを渡すこともできます。
import { createPortal } from 'react-dom';
<div>
<p>This child is placed in the parent div.</p>
{createPortal(
<p>This child is placed in the document body.</p>,
document.body
)}
</div>
このAPIを使って、ページ内の任意の箇所をハイライトするコンポーネントを作成しました。
作ったもの
サンプルを作りました。
今回は、予約一覧にある予約をクリックすると、クリックされた予約に対しスタイルが付与され、オーバーレイ用のコンポーネントが差し込まれるという実装をしました。
createPortal
を使用したHighlight
コンポーネントと、そのコンポーネントを呼び出すReserveListPage
の2つのコンポーネントがあります。
/**
* @param targetDom スタイルをあてる対象
* @param insertDom targetDomをinsertする対象
* @param isOpen ハイライトを表示するかどうか
* @param close ハイライトを閉じるための関数
* @param highlightStyle targetDomに付与するスタイルのオブジェクト
*/
export const Highlight: FC<HighlightProps> = ({
targetDom,
insertDom,
isOpen,
close,
highlightStyle,
}) => {
useEffect(() => {
const addBaseHighlightStyle: Record<string, string> = {
...highlightStyle,
position: "relative",
"z-index": "701",
};
Object.keys(addBaseHighlightStyle).forEach((key) => {
targetDom?.style.setProperty(
key,
isOpen ? addBaseHighlightStyle[key] : ""
);
});
}, [targetDom?.style, highlightStyle, isOpen]);
if (!isOpen) return null;
if (!insertDom) return null;
return createPortal(<Overlay close={close} />, insertDom);
};
const Overlay = ({ close }: { close: () => void }) => (
<div className="overlay" onClick={close} />
);
今回は5つのpropsを受け取るようにしました。
それぞれのpropsの説明は、JSDocに書いてあるとおりです。
簡単に説明すると、isOpen
がtrueであれば、スタイルを付与したい対象(targetDom
)に、highlightStyle
で渡ってきたスタイルを付与し、要素を追加したい対象(insertDom
)を指定するといったことをしています。
次にHighlight
コンポーネントを使用している、ReserveListPage
コンポーネントについてです。
import { useEffect, useRef, useState } from "react";
import { Highlight } from "./Highlight";
const reserveList = [
{
id: 1,
title: "予約",
text: "予約1のテキストです。",
},
];
export const ReserveListPage = () => {
const [open, setOpen] = useState(false);
const [targetIndex, setTargetIndex] = useState<number | null>(null);
const targetRef = useRef<HTMLDivElement>(null);
const insertRef = useRef<HTMLLIElement>(null);
const [targetElement, setTargetElement] = useState<HTMLElement | null>(null);
const [insertElement, setInsertElement] = useState<HTMLElement | null>(null);
useEffect(() => {
setTargetElement(targetRef.current);
setInsertElement(insertRef.current);
}, [targetIndex]);
const handleOpen = (e) => {
setOpen(true);
setTargetIndex(e);
};
return (
<div className="wrapper">
<div className="inner">
<h1>予約一覧</h1>
<ul className="list">
{reserveList.map((reserve, index) => {
const isEditTargetReservation = index === targetIndex;
return (
<li
className="list-item"
key={reserve.id}
ref={isEditTargetReservation ? insertRef : undefined}
>
<div
className="list-item-inner"
ref={isEditTargetReservation ? targetRef : undefined}
>
<button onClick={() => handleOpen(index)}>
{reserve.title}
{reserve.id}
</button>
{isEditTargetReservation && open && <p>{reserve.text}</p>}
</div>
</li>
);
})}
</ul>
</div>
<Highlight
targetDom={targetElement}
insertDom={insertElement}
isOpen={open}
close={() => {
setOpen(false);
}}
highlightStyle={{
"background-color": "white",
border: "2px solid red",
}}
/>
</div>
);
};
targetDom
とinsertDom
はuseRef
を使用して取得し、それぞれHighlight
コンポーネントに渡すようにしています。
この状態で「予約1」をクリックすると、以下のような形でHighlight
コンポーネント内で定義されたOverlay
コンポーネントがli.list-item
に差し込まれ、div.list-item-inner
に対し、highlightStyle
で定義したスタイルが付与されます。
<li class="list-item">
<!-- highlightStyleで指定したstyleが付与される -->
<div class="list-item-inner" style="background-color: white; border: 2px solid red; position: relative; z-index: 701;">
<button>予約1</button>
<p>予約1のテキストです。</p>
</div>
<!-- ここにoverlayが差し込まれる -->
<div class="overlay"></div>
</li>
最後に
いかがだったでしょうか?
createPortal
を使用することで、モーダルを作成したり、アプリでよく見かけるウォークスルーのような表現もできると思います。
みなさんもぜひ使ってみてください!
宣伝
スペースマーケットでは現在エンジニアを募集しております!
ちょっと話を聞いてみたいといったようなカジュアルな面談でも構いませんので、ご興味のある方は是非ご応募お待ちしております!
スペースを簡単に貸し借りできるサービス「スペースマーケット」のエンジニアによる公式ブログです。 弊社採用技術スタックはこちら -> whatweuse.dev/company/spacemarket
Discussion