Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
// Padrão de requisição/resposta: correlaciona respostas com IDs únicos
function sendRequest<T>(
target: Window,
origin: string,
message: { type: string; payload?: unknown },
timeoutMs = 5000
): Promise<T> {
return new Promise((resolve, reject) => {
const id = crypto.randomUUID();
function handleReply(event: MessageEvent) {
if (event.origin !== origin) return;
if (event.data?.correlationId !== id) return;
window.removeEventListener("message", handleReply);
clearTimeout(timer);
resolve(event.data.payload as T);
}
const timer = setTimeout(() => {
window.removeEventListener("message", handleReply);
reject(new Error(`postMessage timeout após ${timeoutMs}ms`));
}, timeoutMs);
window.addEventListener("message", handleReply);
target.postMessage({ ...message, correlationId: id }, origin);
});
}Quando usar isso: Quando mensagens simples de "disparar e esquecer" não são suficientes. Você precisa de pares de requisição/resposta correlacionados, um hook reutilizável para comunicação postMessage, fluxo de dados bidirecional ou roteamento de mensagens para iframes específicos em um layout multi-iframe.
Um painel incorpora um widget de gráfico em um iframe. O pai envia atualizações de dados e recebe eventos de clique de volta do gráfico.
// types/messages.ts - compartilhado entre pai e iframe
export type ParentToChart =
| { type: "DATA_UPDATE"; correlationId?: string; payload: ChartData }
| { type: "SET_OPTIONS"; correlationId?: string; payload: ChartOptions }
| { type: "REQUEST_SELECTION"; correlationId: string };
export type ChartToParent =
| { type: "CHART_CLICK"; payload: { seriesIndex: number; dataIndex: number; value: number } }
| { type: "CHART_READY" }
| { type: "SELECTION_RESPONSE"; correlationId: string; payload: SelectedPoint[] }
| { type: "ERROR"; payload: { message: string } };
export interface ChartData {
labels: string[];
series: { name: string; values: number[] }[];
}
export interface ChartOptions {
animate: boolean;
showLegend: boolean;
colorScheme: "default" | "warm" | "cool";
}
export interface SelectedPoint {
seriesName: string;
label: string;
value: number;
}// hooks/usePostMessage.ts
import { useEffect, useCallback, useRef } from "react";
type MessageHandler<T = unknown> = (
data: T,
event: MessageEvent
) => void;
interface UsePostMessageOptions {
/** Origens permitidas - mensagens de outras origens são silenciosamente descartadas */
allowedOrigins: string[];
/** Filtro opcional: apenas lidar com mensagens onde data.type corresponde */
messageTypes?: string[];
}
export function usePostMessage<TIncoming = unknown>(
handler: MessageHandler<TIncoming>,
options: UsePostMessageOptions
) {
const handlerRef = useRef(handler);
handlerRef.current = handler;
const originsRef = useRef(options.allowedOrigins);
originsRef.current = options.allowedOrigins;
const typesRef = useRef(options.messageTypes);
typesRef.current = options.messageTypes;
useEffect(() => {
function onMessage(event: MessageEvent) {
// Verificação da lista de permissões de origem
if (!originsRef.current.includes(event.origin)) return;
// Filtro de tipo opcional
const types = typesRef.current;
if (types && types.length > 0) {
const msgType = event.data?.type;
if (!types.includes(msgType)) return;
}
handlerRef.current(event.data as TIncoming, event);
}
window.addEventListener("message", onMessage);
return () => window.removeEventListener("message", onMessage);
}, []); // Estável: refs lidam com atualizações sem re-inscrever
// Auxiliar de envio
const send = useCallback(
(target: Window, message: unknown, origin: string) => {
target.postMessage(message, origin);
},
[]
);
return { send };
}import { useRef, useState, useCallback } from "react";
import { usePostMessage } from "./hooks/usePostMessage";
import type { ParentToChart, ChartToParent, ChartData } from "./types/messages";
const CHART_ORIGIN = "https://charts.example.com";
function Dashboard() {
const chartRef = useRef<HTMLIFrameElement>(null);
const [chartReady, setChartReady] = useState(false);
const [lastClick, setLastClick] = useState<string | null>(null);
const { send } = usePostMessage<ChartToParent>(
useCallback((data, event) => {
switch (data.type) {
case "CHART_READY":
setChartReady(true);
break;
case "CHART_CLICK":
setLastClick(
`Série ${data.payload.seriesIndex}, ` +
`ponto ${data.payload.dataIndex}: ${data.payload.value}`
);
break;
case "SELECTION_RESPONSE":
console.log("Pontos selecionados:", data.payload);
break;
case "ERROR":
console.error("Erro no gráfico:", data.payload.message);
break;
}
}, []),
{ allowedOrigins: [CHART_ORIGIN] }
);
function sendData(data: ChartData) {
if (!chartRef.current?.contentWindow) return;
const msg: ParentToChart = { type: "DATA_UPDATE", payload: data };
send(chartRef.current.contentWindow, msg, CHART_ORIGIN);
}
// Requisição/resposta: pede ao gráfico a seleção atual
async function getSelection() {
if (!chartRef.current?.contentWindow) return;
try {
const result = await sendRequest<{ payload: unknown }>(
chartRef.current.contentWindow,
CHART_ORIGIN,
{ type: "REQUEST_SELECTION" },
3000
);
console.log("Seleção:", result);
} catch (err) {
console.error("Requisição de seleção expirou");
}
}
return (
<div>
<h1>Painel</h1>
<div style={{ display: "flex", gap: 8 }}>
<button
disabled={!chartReady}
onClick={() =>
sendData({
labels: ["Jan", "Fev", "Mar"],
series: [{ name: "Receita", values: [100, 150, 130] }],
})
}
>
Enviar Dados
</button>
<button disabled={!chartReady} onClick={getSelection}>
Obter Seleção
</button>
</div>
{lastClick && <p>Último clique: {lastClick}</p>}
<iframe
ref={chartRef}
src={`${CHART_ORIGIN}/chart-widget`}
title="Widget de Gráfico"
style={{ width: "100%", height: 400, border: "1px solid #e2e8f0" }}
/>
</div>
);
}import { useEffect, useCallback, useState } from "react";
import { usePostMessage } from "./hooks/usePostMessage";
import type { ParentToChart, ChartToParent, ChartData, SelectedPoint } from "./types/messages";
const PARENT_ORIGIN = "https://dashboard.example.com";
function ChartWidget() {
const [data, setData] = useState<ChartData | null>(null);
const [selected, setSelected] = useState<SelectedPoint[]>([]);
const { send } = usePostMessage<ParentToChart>(
useCallback((msg, event) => {
const reply = (response: ChartToParent) => {
(event.source as Window).postMessage(response, event.origin);
};
switch (msg.type) {
case "DATA_UPDATE":
setData(msg.payload);
break;
case "SET_OPTIONS":
// aplicar opções do gráfico...
break;
case "REQUEST_SELECTION":
// Responder com a seleção atual, preservando correlationId
reply({
type: "SELECTION_RESPONSE",
correlationId: msg.correlationId,
payload: selected,
});
break;
}
}, [selected]),
{ allowedOrigins: [PARENT_ORIGIN] }
);
// Notificar o pai que estamos prontos
useEffect(() => {
if (!window.parent || window.parent === window) return;
const msg: ChartToParent = { type: "CHART_READY" };
window.parent.postMessage(msg, PARENT_ORIGIN);
}, []);
function handleBarClick(seriesIndex: number, dataIndex: number, value: number) {
const clickMsg: ChartToParent = {
type: "CHART_CLICK",
payload: { seriesIndex, dataIndex, value },
};
window.parent.postMessage(clickMsg, PARENT_ORIGIN);
}
if (!data) return <p>Aguardando dados...</p>;
return (
<div style={{ padding: 16 }}>
<h3>Widget de Gráfico</h3>
{data.series.map((series, si) => (
<div key={series.name}>
<h4>{series.name}</h4>
<div style={{ display: "flex", gap: 4, alignItems: "flex-end", height: 200 }}>
{series.values.map((val, di) => (
<div
key={di}
onClick={() => handleBarClick(si, di, val)}
style={{
width: 40,
height: `${(val / Math.max(...series.values)) * 100}%`,
background: "#3b82f6",
cursor: "pointer",
display: "flex",
alignItems: "flex-end",
justifyContent: "center",
color: "white",
fontSize: 12,
paddingBottom: 4,
}}
>
{val}
</div>
))}
</div>
<div style={{ display: "flex", gap: 4 }}>
{data.labels.map((label) => (
<div key={label} style={{ width: 40, textAlign: "center", fontSize: 11 }}>
{label}
</div>
))}
</div>
</div>
))}
</div>
);
}postMessage fire-and-forget é insuficiente quando você precisa de um valor de retorno. A solução é anexar um correlationId único a cada requisição e fazer com que o respondedor o repita.crypto.randomUUID() para IDs. Ele está disponível em todos os navegadores modernos e é criptograficamente aleatório.resolve e reject para evitar vazamentos de memória.send é um callback estável (envolto em useCallback com dependências vazias) para que possa ser passado como prop sem causar re-renderizações.messageTypes permite que você escopo o handler de um componente apenas para tipos de mensagem relevantes, o que mantém os handlers focados.message dispara para cada postMessage de cada iframe. Você precisa rotear as mensagens para o handler correto.event.source contra refs de iframe. Esta é a abordagem mais confiável.source ou channel ao seu protocolo de mensagem para que os handlers possam filtrar por origem lógica.function useIframeMessage(
iframeRef: React.RefObject<HTMLIFrameElement | null>,
origin: string,
handler: (data: unknown) => void
) {
const handlerRef = useRef(handler);
handlerRef.current = handler;
useEffect(() => {
function onMessage(event: MessageEvent) {
if (event.origin !== origin) return;
// Lidar apenas com mensagens deste iframe específico
if (event.source !== iframeRef.current?.contentWindow) return;
handlerRef.current(event.data);
}
window.addEventListener("message", onMessage);
return () => window.removeEventListener("message", onMessage);
}, [origin, iframeRef]);
}
// Uso: cada iframe recebe seu próprio handler com escopo
function MultiIframePage() {
const chartRef = useRef<HTMLIFrameElement>(null);
const formRef = useRef<HTMLIFrameElement>(null);
useIframeMessage(chartRef, "https://charts.example.com", (data) => {
console.log("Do gráfico:", data);
});
useIframeMessage(formRef, "https://forms.example.com", (data) => {
console.log("Do formulário:", data);
});
return (
<>
<iframe ref={chartRef} src="https://charts.example.com/widget" title="Gráfico" />
<iframe ref={formRef} src="https://forms.example.com/widget" title="Formulário" />
</>
);
}O algoritmo de clonagem estruturada suporta mais tipos do que JSON, mas ainda tem limites:
| Suportado | Não Suportado |
|---|---|
| Primitivos (string, number, boolean, null, undefined) | Funções |
| Objetos e arrays simples | Nós DOM (Element, Document, etc.) |
Date | Symbol |
Map, Set | WeakMap, WeakSet |
RegExp | Instâncias de classe (protótipo é perdido) |
ArrayBuffer, arrays tipados (Uint8Array, etc.) | Objetos Error (em alguns navegadores) |
Blob, File, FileList | Getters, setters, descritores de propriedade |
ImageBitmap, ImageData | Objetos Proxy |
| Objetos aninhados com referências circulares | Closures |
Detalhes chave:
class User { getName() {} } se torna um objeto simples do outro lado. Apenas propriedades enumeráveis próprias sobrevivem.JSON.stringify, a clonagem estruturada pode serializar objetos com ciclos.ArrayBuffer é copiado por padrão. Use o parâmetro transfer para transferir a propriedade (zero-cópia) em vez disso. Veja o guia avançado.Error têm suporte de clonagem inconsistente entre navegadores. Envie { message: error.message, stack: error.stack } em vez disso.useEffect, você lerá valores obsoletos. Use um ref para a função handler (como mostrado no hook) ou inclua o estado no array de dependências (que re-inscreve o listener a cada mudança).contentWindow é nulo até que o elemento iframe esteja no DOM e tenha começado a carregar. Sempre verifique se é nulo antes de chamar postMessage.contentWindow ainda existe, mas agora aponta para o novo documento. Qualquer estado no documento antigo se foi. Você precisa que a nova página envie uma nova mensagem READY.postMessage lança um DataCloneError. Isso pode ser surpreendente porque o erro ocorre no lado do remetente, não no receptor.message pode receber mensagens de iframes de extensão que você não criou. Sempre valide a origem e a forma da mensagem.| Abordagem | Quando Usar |
|---|---|
| MessageChannel | Porta bidirecional dedicada para exatamente dois endpoints (sem broadcast, sem verificação de origem necessária após a configuração) |
| BroadcastChannel | Fan-out de mesma origem para todas as abas e janelas (sem cross-origin) |
| Comlink (biblioteca) | RPC de alto nível sobre postMessage para workers e iframes; esconde o protocolo completamente |
Parâmetros de URL src do iframe | Configuração inicial única (sem comunicação contínua) |
| Estado compartilhado via servidor | Quando iframes estão em domínios diferentes e precisam de estado compartilhado persistente |
postMessage é fire-and-forget sem valor de retorno embutido.correlationId único (via crypto.randomUUID()) permite que você combine uma resposta com a requisição original.useRef, atualizado a cada renderização via handlerRef.current = handler.useEffect lê do ref, então ele sempre chama o handler mais recente sem re-inscrever o listener.export function usePostMessage<TIncoming = unknown>(
handler: (data: TIncoming, event: MessageEvent) => void,
options: { allowedOrigins: string[] }
) {
// TIncoming restringe event.data dentro do handler
}event.source contra refs de iframe para identificar qual iframe enviou a mensagem.source ou channel ao seu protocolo de mensagem para roteamento lógico.MessageChannel para criar uma porta dedicada por iframe (abordado no guia avançado).class User { getName() {} } se torna um objeto simples no lado receptor.postMessage(), não no receptor.structuredClone() localmente para testar se seu payload é clonável.contentWindow ainda existe, mas agora aponta para o novo documento.READY.const send = useCallback(
(target: Window, message: unknown, origin: string) => {
target.postMessage(message, origin);
},
[] // dependências vazias = referência estável
);string[] para os nomes dos tipos de mensagem permitidos.event.data?.type contra este array antes de invocar o handler.window.postMessage.message recebe mensagens de iframes de extensão.type).How to Send Data From iframe To Parent Page - JavaScript postMessage Tutorial
Window postMessage() protocol using React - #1 JavaScript Tutorials
Revisado por Chris St. John·Última atualização: 19 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥