異なるDOMノードにレンダリングする2つの React Component 間で状態を共有する

結論

Portalを使う ja.reactjs.org

解説

既存のサイトを部分的に React に置き換えている場合、それぞれ異なるDOMノードにレンダリングするコンポーネント間で状態を共有したい場面に遭遇することがあります。

例えば、次のようなヘッダー、サイドバー、コンテンツを表示するサイトを想像してみてください。

f:id:dev-moyashi:20200531155557p:plain

<html>
  <body>
    <div id="root"></div>
    <header class="header"></header>
    <aside class="sidebar"></aside>
    <main class="content"></main>
  </body>
</html

このサイトのヘッダーとサイドバー部分を React で置き換えているとします。

ヘッダーにはサイドバーの表示/非表示を切り替えるボタンを設置し、クリックするたびにサイドバーが開閉します。

このとき、別々のDOMノードにコンポーネントレンダリングしようとすると以下のようになります。

ReactDOM.render(<Header />, document.querySelector(".header"));
ReactDOM.render(<Sidebar />, document.querySelector(".sidebar"));

ただし、ReactDom.render() で別々のDOMノードにレンダリングすると、Header と Sidebar コンポーネント間でサイドバーの開閉状態を共有することができません。

そこで使用するのが React の Portal です。

Portalを使えば、Virtual DOM 上では通常の親-子コンポーネントと同じように props を渡すことができて、実DOM上では親コンポーネント外の箇所に子コンポーネントレンダリングすることができます。

使い方は ReactDOM.createPortal の第1引数にコンポーネント、第2引数にDOMノードを指定するだけです。

const Index = () => {
  const [isOpenSidebar, setOpenSidebar] = useState(false);

  const headerPortal = ReactDOM.createPortal(
    <Header setOpenSidebar={setOpenSiebar} />,
    document.querySelector(".header")
  );

  const sidebarPortal = ReactDOM.createPortal(
    <Sidebar isOpenSidebar={isOpenSidebar} />,
    document.querySelector(".sidebar")
  );

  return (
    <>
      {headerPortal}
      {sidebarPortal}
    </>
  );
};

ReactDOM.render(<Index />, document.getElementById("root")); // 任意のDOMノード

参考