概要
- React
- iframe の親子間でメッセージをやり取りするには
window.postMessage()
を用いる
React で iframe
そもそも iframe タグですが、他のHTMLタグと同じようにJSXで書けます。
export default function Page() { // 埋め込み return ( <iframe src='/embed' /> ) }
ここでは同じドメインの /embed ページを読み込んでます。
親から子にメッセージを渡す
iframe では window.postMessage()
でメッセージのやり取りができます (MDN)
ここでは親から子にデータを渡してみましょう。
親
まずは親のコードです。
import { useRef, MouseEventHandler } from 'react' export default function Page() { const child = useRef<HTMLIFrameElement>(null) // ボタンをクリックしたら.. const handleClick: MouseEventHandler<HTMLButtonElement> = (e) => { e.preventDefault() // 子に hey と言うメッセージを投げる child.current?.contentWindow?.postMessage('hey', 'http://localhost:3000') } return ( <> <iframe src='/embed' ref={child} /> <button onClick={handleClick}>aa</button> </> ) }
同じドメインの /embed ページを読み込んでます。
またボタンを押したら子に hey
というメッセージを送ります。
子
続いて子のコードです。messageイベント で受け取れます
export default function Page() { window.addEventListener('message', (event) => { console.log(event.data) // hey }) return <>a</> }
実行するとコンソールに hey
という文字列が流れてきます
子から親にメッセージを渡す
逆をしてみます。
親
まず受信側(親)のコードです
export default function Page() { window.addEventListener('message', (event) => { console.log(event.data) // hey }) return ( <iframe src='/embed' /> ) }
子
続いて送信側(子)のコードです
export default function Page() { window.parent.postMessage('hey') return <>a</> }
window.parent で親の window への参照を取得できます。
event を絞り込む
何気なく
window.addEventListener('message', (event) => {})
というコードを書いてますが、ライブラリによってはこのイベント (message) を使っており、コールバック関数にそのメッセージが入ってくることがあります。例えば react-devtools
のメッセージが入ってきます (参考)
ここでは送信データを工夫します。次のような type を定義しガードします
export type AppMessage = { source: 'me' // 一意になるよう.. text: string } export const isAppEvent = (event: MessageEvent<any>): event is MessageEvent<AppMessage> => { return typeof event.data === 'object' && event.data?.source === 'me' && typeof event.data?.text === 'string' }
ポイントは source です。一意の値を入れ判別できるようにしました
これを使ってみます。まずは送信側(親)です
export default function Page() { const child = useRef<HTMLIFrameElement>(null) const handleClick: MouseEventHandler<HTMLButtonElement> = (e) => { e.preventDefault() // メッセージ const message: AppMessage = {source: 'me', text: 'hey'} child.current?.contentWindow?.postMessage(message, 'http://localhost:3000') } return ( <> <iframe src='/embed' ref={child} /> <button onClick={handleClick}>aa</button> </> ) }
続いて受信側(親)です
export default function Page() { window.addEventListener('message', (event) => { // type guard if (isAppEvent(event)) { console.log(event.data.text) // hey } }) return ( <iframe src='/embed' /> ) }
親と子とで type を共有してます
React Hooks を使う
window.addEventListener
ですが React Hooks のライブラリがあります
- usehooks-ts の useEventListener
- @react-hook/event の useEvent (参考)
どちらも問題なく動きました
終わりに
メッセージの送受信は工夫の余地があります。
おそらくマイクロフロントエンドの課題感に近そうに感じます。
[余談] そもそもなぜ iframe を使うのか
iframe を使う動機ですが、私の場合 JavaScript のグローバル空間汚染を隔離するためでした。
GoでWasmアプリを作ってまして、、
プログラムに JavaScript の変数へアクセスするコードが混じってました。
func main() { ch := make(chan struct{}) // js のグローバル変数を宣言している js.Global().Set("callName", js.FuncOf(callName)) <-ch }
このコードは JavaScript のグローバル空間を汚染します。
そのため iframe で隔離したくなった次第です。