課題
親ウィンドウで印刷ボタンを押下した際に、親ウィンドウで取得した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>
</>
);
};
レンダリングが完了したことを通知することで無事データの受け渡しを完了することができた。