0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Window.postMessage()で子ウィンドウにデータを送信する

Last updated at Posted at 2024-11-16

課題

親ウィンドウで印刷ボタンを押下した際に、親ウィンドウで取得したID配列(今回はAPI通信実装省略)を子ウィンドウにデータを送信したい。
子ウィンドウにデータを送信する方法としては、URL、セッションストレージなどがあげられるが、今回はwindow.postMessage()でデータを送信する方法を考える。

ファイル構成

└─ root/
    └─ src/
        ├─ Parent/
        │   ├─ index.tsx
        ├─ Child/
        │   ├─ index.tsx
        ├─ Parent/
        │   └─ index.tsx 
        ├─ MiniWindow/ 
        │   ├─ index.tsx
        │   └─ hooks.ts
        ├─ App.tsx
        └── main.tsx      

事前準備

AppRouters.tsx

ルーティング設定を行う

import { BrowserRouter, Route, RouteProps, Routes } from "react-router-dom";
import { App } from "./App";
import { Child } from "./Child";

export const AppRoutes = () => {
 const routes = [
   {
     path: "/",
     Component: App,
   },
   {
     path: "/child",
     Component: Child,
   },
 ] as const satisfies RouteProps[];

 return (
   <BrowserRouter>
     <Routes>
       {routes.map(({ path, Component }, i) => (
         <Route key={i} path={path} element={<Component />} />
       ))}
     </Routes>
   </BrowserRouter>
 );
};

App.tsx

import "./App.css";
import { Parent } from "./Parent";

export const App = () => {
 return (
   <>
     <Parent />
   </>
 );
};

Parent/index.tsx

印刷したい画面のプレビューを表示

import { useState } from "react";
import { MiniWindow } from "../MiniWindow";

export const Parent = () => {
 const requestIds = ["1", "2", "3"];

 const handleClick = () => {
   const win = window.open("child", undefined, "width=600,height=800");
 };

 return (
   <>
     <div>プレビュー</div>
     <button onClick={handleClick}>印刷する</button>
     <div className={styles.container}>
       <MiniWindow requestIds={requestIds} />
     </div>
   </>
 );
};

Child/index.tsx

子ウィンドウとして印刷画面を表示

import { useEffect, useState } from "react";
import { MiniWindow } from "../MiniWindow";
import { PdfInfo } from "../MiniWindow/hooks";

export const Child = () => {
 const [requestIds, setRequestIds] = useState<string[] | null>(null);
 const [pdfData, usePdfData] = useState<PdfInfo[] | null>(null);

 useEffect(() => {
   if (pdfData) {
     window.print();
   }
 }, [pdfData]);

 if (!requestIds) return null;

 return (
     <div>
       <MiniWindow requestIds={requestIds} onGetData={usePdfData} />
     </div>
 );
};

MiniWindow/index.tsx

ParentとChild(親ウィンドウと子ウィンドウ)共通で表示する印刷画面のコンポーネント

import { useEffect } from "react";
import {
 doGetWindowInfo,
 PdfInfo,
 pdfInfoListVar,
 usePdfInfoList,
} from "./hooks";

type Props = {
 requestIds: string[];
 onGetData?: (info: PdfInfo[]) => void;
};

export const MiniWindow: React.FC<Props> = ({ requestIds, onGetData }) => {
 const { pdfInfoList } = usePdfInfoList();

 useEffect(() => {
   const info = doGetWindowInfo(requestIds);
   pdfInfoListVar(info);
 }, []);

 useEffect(() => {
   if (pdfInfoList) {
     onGetData?.(pdfInfoList);
   }
 }, [pdfInfoList]);

 return (
   <div className={styles.container}>
     <h1>【見出し】</h1>
     <>
       {pdfInfoList.map(({ id, title, message }) => (
         <div key={id}>
           <h2>{title}</h2>
           <div>{message}</div>
         </div>
       ))}
     </>
   </div>
 );
};

MiniWindow/hooks.ts

import { makeVar, useReactiveVar } from "@apollo/client";

export type PdfInfo = {
 id: string;
 title: string;
 message: string;
};

// PDFで表示するための値を取得
export const doGetWindowInfo = (ids: string[]): PdfInfo[] => {
 return ids.map((id) => ({
   id,
   title: "PDF 見出し",
   message: `PDFメッセージ ${id}`,
 }));
};

// PDFで表示するための値をキャッシュ
export const pdfInfoListVar = makeVar<PdfInfo[]>([]);

export const usePdfInfoList = () => ({
 pdfInfoList: useReactiveVar(pdfInfoListVar),
});

実装

1. ParentへpostMessage処理の追加

Parent/index.tsx
import { useState } from "react";
import { MiniWindow } from "../MiniWindow";

export const Parent = () => {
 const requestIds = ["1", "2", "3"];

 const handleClick = () => {
   const win = window.open("child", undefined, "width=600,height=800");
   if (win) {
       // windowが開くのを待機して子ウィンドウにデータを送信する
       win.onload = () => {
           win.postMessage({ requestIds: requestIds });
         }
   }
 };

 return (
   <>
     <div>プレビュー</div>
     <button onClick={handleClick}>印刷する</button>
     <div className={styles.container}>
       <MiniWindow requestIds={requestIds} />
     </div>
   </>
 );
};

2. Child側にpostMessageで送信された値を受け取るイベントリスナーを追加

子ウィンドウとして印刷画面を表示

Child/index.tsx
import { useEffect, useState } from "react";
import { MiniWindow } from "../MiniWindow";
import { PdfInfo } from "../MiniWindow/hooks";

export const Child = () => {
 const [requestIds, setRequestIds] = useState<string[] | null>(null);
 const [pdfData, usePdfData] = useState<PdfInfo[] | null>(null);

 useEffect(() => {
   if (pdfData) {
     window.print();
   }
 }, [pdfData]);

// requestIdをうけとるイベントリスナー
 window.addEventListener("message", (e) => {
   if (e.data.requestIds) {
     setRequestIds(e.data.requestIds);
   }
 });

 if (!requestIds) return null;

 return (
     <div>
       <MiniWindow requestIds={requestIds} onGetData={usePdfData} />
     </div>
 );
};

これで印刷ボタンを押してみた結果...ページの表示が空

window.onload()でウィンドウが立ち上がるのを待機をしているものの、Reactのレンダリングが完了する前にデータを送信してしまっているため正常にデータが送れていないっぽい。

3. 子ウィンドウがレンダリングしたことをwindow.opener.postMessage()で親に対して通知する

Child/index.tsx
import { useEffect, useState } from "react";
import { MiniWindow } from "../MiniWindow";
import { PdfInfo } from "../MiniWindow/hooks";

export const Child = () => {
 const [requestIds, setRequestIds] = useState<string[] | null>(null);
 const [pdfData, usePdfData] = useState<PdfInfo[] | null>(null);

 // 親ウィンドウに対してレンダリングが完了したことを通知する
 useEffect(() => {
   const win = window.opener as Window;
   win.postMessage({ isReceived: true });
 }, []);

 useEffect(() => {
   if (pdfData) {
     window.print();
   }
 }, [pdfData]);

 // requestIdをうけとるイベントリスナー
 window.addEventListener("message", (e) => {
   if (e.data.requestIds) {
     setRequestIds(e.data.requestIds);
   }
 });

 if (!requestIds) return null;

 return (
   <div>
     <MiniWindow requestIds={requestIds} onGetData={usePdfData} />
   </div>
 );
};

4. メッセージ受信後にIDを子ウィンドウに送信する

Parent/index.tsx
import { useState } from "react";
import { MiniWindow } from "../MiniWindow";

export const Parent = () => {
 // 開いている子ウィンドウの情報を保持
 const [openWindow, setOpenWindow] = useState<Window | null>(null);
 const requestIds = ["1", "2", "3"];

 const handleClick = () => {
   const win = window.open("child", undefined, "width=600,height=800");
   // 子ウィンドウの情報をローカルステートに保持
   setOpenWindow(win);
 };

 useEffect(() => {
   // 子ウィンドウから受信時、IDを送信
   window.addEventListener("message", (e) => {
     if (openWindow && e.data.isReceived) {
       openWindow.postMessage({ requestIds: requestIds });
       // データ送信完了後に子ウィンドウの情報を削除
       setOpenWindow(null)
     }
   });
 }, [openWindow]);

 return (
   <>
     <div>プレビュー</div>
     <button onClick={handleClick}>印刷する</button>
     <div className={styles.container}>
       <MiniWindow requestIds={requestIds} />
     </div>
   </>
 );
};

レンダリングが完了したことを通知することで無事データの受け渡しを完了することができた。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?