ReactのPortalをある指定したコンポーネントの上に表示させる

# はじめに

いつかやろうと思っていたReactに最近向き合って苦戦しております。

今回、Portalを用いて、あるコンポーネントを特定のコンポーネントの上にのみ描画させることができたので、記事にします。

Reactのバージョンは18です。

# やりたいこと
 
まず以下の画面のように、`Header`(light pink), `SideBar`(light green), `Main`(light blue), `Footer`(light gray)の4つのコンポーネントを持つ画面を作ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/255755/3c793bad-6c25-692d-a4ba-28c36dc3a480.png)

そこで、`Main`にあるボタンをクリックすると、`Main`コンポーネントの上にのみPortalのコンポーネント`Portal`(white)を表示させてやりたいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/255755/806794ee-d2a3-ead4-0e0f-ce070cec1852.png)

# 実装

まず、各コンポーネントを作っていきます。
Header,Footerの縦は10vh, SideBar, Mainの縦は80vhで、SideBarの横は10vw, Mainを90vwとします。
SideBarとMainはFlexで設置します。

大元となる`App.tsx`です。SideBarコンポーネント内にMainコンポーネントを設置します。
```App.tsx
import { Footer } from './Footer';
import { Header } from './Header';
import { SideBar } from './SideBar';

function App() {
  return (
    <>
      <Header />
      <SideBar />
      <Footer />
    </>
  );
};
export default App;
```

次にHeader, Footerを作ります。
```Header.tsx
export const Header = () => {
    return (
        <div style={{height: "10vh", backgroundColor: "lightpink"}}>
            Header
        </div>
        
    );
};
```

```Footer.tsx
export const Footer = () => {
    return(
        <div style={{height: "10vh", backgroundColor: "lightgray"}}>
            Footer
        </div>
    )
}
```

SideBarはFlexレイアウトとします。

```SideBar.tsx
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を作ります。

```Main.tsx
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部分です。

```Portal.tsx
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ガチ初心者ですが、なんとかこのようにして狙った挙動をつくることができました。
間違えや助言等ありましたらコメント頂ければ幸いです。

# 参考

https://nishinatoshiharu.com/create-portal-overview/#propschildren