はじめに
いつかやろうと思っていたReactに最近向き合って苦戦しております。
今回、Portalを用いて、あるコンポーネントを特定のコンポーネントの上にのみ描画させることができたので、記事にします。
Reactのバージョンは18です。
やりたいこと
まず以下の画面のように、Header
(light pink), SideBar
(light green), Main
(light blue), Footer
(light gray)の4つのコンポーネントを持つ画面を作ります。
そこで、Main
にあるボタンをクリックすると、Main
コンポーネントの上にのみPortalのコンポーネントPortal
(white)を表示させてやりたいです。
実装
まず、各コンポーネントを作っていきます。
Header,Footerの縦は10vh, SideBar, Mainの縦は80vhで、SideBarの横は10vw, Mainを90vwとします。
SideBarとMainはFlexで設置します。
大元となるApp.tsx
です。SideBarコンポーネント内にMainコンポーネントを設置します。
import { Footer } from './Footer';
import { Header } from './Header';
import { SideBar } from './SideBar';
function App() {
return (
<>
<Header />
<SideBar />
<Footer />
</>
);
};
export default App;
次にHeader, Footerを作ります。
export const Header = () => {
return (
<div style={{height: "10vh", backgroundColor: "lightpink"}}>
Header
</div>
);
};
export const Footer = () => {
return(
<div style={{height: "10vh", backgroundColor: "lightgray"}}>
Footer
</div>
)
}
SideBarはFlexレイアウトとします。
import { Main } from "./Main"
export const SideBar = () => {
return (
<div style={{display: "flex", height: "80vh"}}>
<div style={{width: "10vw", backgroundColor: "lightgreen"}}>
Sidebar
</div>
<Main />
</div>
)
}
Mainコンポーネントです。ここのポイントはid
属性にmain
を加えてあげることです。
ボタンと、そのhandlerを作ります。
import { FC, useState } from "react";
import { Portal } from "./Portal";
export const Main: FC = () => {
const [showPortal, setShowPortal] = useState(false);
const handleClick = () => {
showPortal ? setShowPortal(false) : setShowPortal(true);
};
return (
<div id="main" style={{backgroundColor: "lightblue", width: "90vw"}}>
{ showPortal && <Portal />}
<div style={{position: "absolute"}}>
Main<br />
<button onClick={handleClick}>Show/Hide portal</button>
</div>
</div>
);
};
そして、最後に肝となるPortal部分です。
import { FC, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
const createElement = (): HTMLElement => {
const el = document.createElement("div");
el.setAttribute("id", "portal");
el.setAttribute("style","height: 100%");
return el;
};
export const Portal: FC = () => {
const [showPortal, setShowPortal] = useState(false);
useEffect(() => {
const el = document.querySelector<HTMLElement>("#portal") ?? createElement();
const el_main = document.querySelector<HTMLElement>("#main")!;
el_main.appendChild(el);
},[]);
useEffect(() => {
setShowPortal(true);
},[]);
if (!showPortal) {
return null;
}
return createPortal(
<div style={{backgroundColor: "white", height: "100%", textAlign: "right"}}>
Portal
</div>
,document.getElementById("portal")!);
};
よくあるPortalの例では、Modalを全画面に出力させるというもので、大体はroot
の上に描画させるというものでしたが、ここではMain
コンポーネントの上にのみ出力したいので、Main
の出力にid: main
を割り振って、それに対してportalをappendChild
をしてやることを思いつきました。
また、Portalのサイズについてですが、これを見てわかるように親はMain
コンポーネントであるので、Mainに対して高さを100%とすれば良いです。これを実現するにはcreateElement
をするときにstyle
属性も与えてしまうことで解決できました。
最後に
Reactガチ初心者ですが、なんとかこのようにして狙った挙動をつくることができました。
間違えや助言等ありましたらコメント頂ければ幸いです。
参考