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

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

## ファイル構成

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

 ## 事前準備
 ### AppRouters.tsx
ルーティング設定を行う
 ``` 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
 ``` tsx
import "./App.css";
import { Parent } from "./Parent";

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

### Parent/index.tsx
印刷したい画面のプレビューを表示
 ``` 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
子ウィンドウとして印刷画面を表示
 ``` 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(親ウィンドウと子ウィンドウ)共通で表示する印刷画面のコンポーネント
 ``` tsx
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
 ``` 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>
    </>
  );
};
 ```

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