SPA(React) で親 Window から子 Window へのデータの受け渡しがしたい

React
ReactTypeScript

SPA(Single Page Application)だけど別ウィンドウでも開きたい、かつ、起動元の親ウィンドウのデータを起動された子ウィンドウで参照したい場合の方法です。

候補は以下

  1. URL パラメータ渡し
  2. LocalStorage
  3. グローバル変数

前提

以下で作ったものをベースに作業(Vite 4.0.0 + React 18.0.9)しました。

今回は React Router をインストールして、別 URLに遷移するときに値渡しを試しました。

検証コード

遷移の定義。今回は / から /child1 へ遷移しています。

const router = createBrowserRouter(
  createRoutesFromElements(
    <Route path="/" element={<App />}>
      <Route path="child1" element={<Child1 />} />
    </Route>,
  ),
)
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>,
)

親コンポーネント(App)。今回検証した3パターン全部含めています。説明は後ほど個別に。

import { useState } from 'react'
import { Outlet } from 'react-router-dom'
import './App.css'
import reactLogo from './assets/react.svg'

function App() {
  const [count, setCount] = useState(0)

  return (
    <div className="App">
      <div>
        <a href="https://vitejs.dev" target="_blank" rel="noreferrer">
          <img src="/vite.svg" className="logo" alt="Vite logo" />
        </a>
        <a href="https://reactjs.org" target="_blank" rel="noreferrer">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
        {/* URL パラメータに指定する */}
        <div>
          <button
            onClick={() =>
              window.open(
                `http://localhost:5173/child1?counter=${count}`,
                'Child1',
              )
            }
          >
            /Child1 へ - URLパラメータ渡し
          </button>
        </div>
        {/* LocalStorage に指定する */}
        <div>
          <button
            onClick={() => {
              localStorage.setItem('params', JSON.stringify({ count }))
              window.open('http://localhost:5173/child1', 'Child1')
            }}
          >
            Child1 へ - LocalStorage 経由
          </button>
        </div>
        <div>
          <NewWindow
            url="http://localhost:5173/child1"
            name=""
            params={{ count }}
          ></NewWindow>
        </div>
      </div>
      <hr />
      <div>
        <Outlet />
      </div>
    </div>
  )
}

function NewWindow(props: { url: string; name: string; params: any }) {
  const w = window.self as typeof window & { openerParams: string }
  w.openerParams = JSON.stringify(props.params)
  const onClickHandler = () => w.open(props.url, props.name)

  return <button onClick={onClickHandler}>Child1 へ - グローバル変数</button>
}
export default App

子コンポーネント。渡された値を表示するだけです。

import { useLocation } from 'react-router-dom'

export default function Child1() {
  const location = useLocation()
  const localStorageParam = localStorage.getItem('params')
  const opener = window.opener

  return (
    <>
      <div>Child1</div>
      <table border={1}>
        <tbody>
          <tr>
            <td>Window Name</td>
            <td>{window.name}</td>
          </tr>
          <tr>
            <td>URL params</td>
            <td>{location.search}</td>
          </tr>
          <tr>
            <td>LocalStorage</td>
            <td>{localStorageParam}</td>
          </tr>
          <tr>
            <td>グローバル変数</td>
            <td>{opener.openerParams}</td>
          </tr>
        </tbody>
      </table>
    </>
  )
}

ここから結果です。

1. URLパラメータ渡し

これは言うまでもないですね。

          <button
            onClick={() =>
              window.open(
                `http://localhost:5173/child1?counter=${count}`,
                'Child1',
              )
            }
          >
            /Child1 へ - URLパラメータ渡し
          </button>

結果です。左が親、右が起動後の子です。
親の count 値 5 が URL パラメータとして渡っており、react-router-dom の useLocation から取得できています。

URLパラメータ渡し場合の結果

この方法は簡単につきます。
デメリットはパラメータが長い場合でしょうか。とはいえ IE 問題もなくなったので 4000-5000 文字くらいは URL文字列としてはいけたという記事もあったので、これで十分な場合な場合も多そう。あとは、利用者がURLコピーするような場合は長すぎはだめですね。

2. LocalStorage

こちらも言うまでもないですね。window.open 前にセットしています。

          <button
            onClick={() => {
              localStorage.setItem('params', JSON.stringify({ count }))
              window.open('http://localhost:5173/child1', 'Child1')
            }}
          >
            Child1 へ - LocalStorage 経由
          </button>

結果です。1と同じく左が親、右が起動後の子です。ちゃんと参照できます。

LocalStorage を使った場合の結果

こちらの方法も簡単ですね。
デメリットは、LocalStorage なのでブラウザ使える人はだれでもアクセスできる点、使い方を決めておかないとゴミが残ったままになったり、意図しないタイミングで更新されて期待してない値になっちゃったりする可能性への対応が必要です。

3. グローバル変数

グローバル変数というだけで微妙…となりがちですが、一概に否定せずケースによっては使い所もあるのかなとは思っています。

実装としては、window オブジェクト自体にパラメータをセットしつつ、子から opener 経由で値を参照しています。react-window-opener の実装もこんな感じぽいです。

        <div>
          <NewWindow
            url="http://localhost:5173/child1"
            name=""
            params={{ count }}
          ></NewWindow>
        </div>
~~~
function NewWindow(props: { url: string; name: string; params: any }) {
  const w = window.self as typeof window & { openerParams: string }
  w.openerParams = JSON.stringify(props.params)
  const onClickHandler = () => w.open(props.url, props.name)

  return <button onClick={onClickHandler}>Child1 へ - グローバル変数</button>
}

結果です。ちゃんと渡ってますね。

グローバル変数を使った場合の結果

他の2つよりはコードは書きますが独自プロパティをセットしているだけです。
デメリットはやはりグローバル変数を触る点と LocalStorage 同様ライフサイクルをちゃんと管理する必要がある。

まとめ

どの方法でもデータを渡すことはできました。管理の煩雑さを考えれば 1つ目がいいのかな、と。
他に良い方法があれば指摘お願いします。

コメント