Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
// Patrón Request/response: correlaciona respuestas con 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 after ${timeoutMs}ms`));
}, timeoutMs);
window.addEventListener("message", handleReply);
target.postMessage({ ...message, correlationId: id }, origin);
});
}Cuándo usarlo: Cuando los mensajes simples fire-and-forget no son suficientes. Necesitas pares request/response correlacionados, un hook reutilizable para comunicación postMessage, flujo de datos bidireccional, o enrutamiento de mensajes a iframes específicos en un layout multi-iframe.
Un dashboard integra un widget de gráfico en un iframe. El padre envía actualizaciones de datos y recibe eventos de clic del gráfico.
// types/messages.ts - compartido entre padre 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 {
/** Orígenes permitidos - los mensajes de otros orígenes se descartan silenciosamente */
allowedOrigins: string[];
/** Filtro opcional: solo maneja mensajes donde data.type coincide */
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) {
// Verificación de lista blanca de origen
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);
}, []); // Estable: refs manejan actualizaciones sin re-suscribirse
// Ayudante Send
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(
`Series ${data.payload.seriesIndex}, ` +
`point ${data.payload.dataIndex}: ${data.payload.value}`
);
break;
case "SELECTION_RESPONSE":
console.log("Selected points:", data.payload);
break;
case "ERROR":
console.error("Chart error:", 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);
}
// Request/response: pide al gráfico la selección actual
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("Selection:", result);
} catch (err) {
console.error("Selection request timed out");
}
}
return (
<div>
<h1>Dashboard</h1>
<div style={{ display: "flex", gap: 8 }}>
<button
disabled={!chartReady}
onClick={() =>
sendData({
labels: ["Jan", "Feb", "Mar"],
series: [{ name: "Revenue", values: [100, 150, 130] }],
})
}
>
Send Data
</button>
<button disabled={!chartReady} onClick={getSelection}>
Get Selection
</button>
</div>
{lastClick && <p>Last click: {lastClick}</p>}
<iframe
ref={chartRef}
src={`${CHART_ORIGIN}/chart-widget`}
title="Chart Widget"
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 opciones del gráfico...
break;
case "REQUEST_SELECTION":
// Responde con la selección actual, preservando correlationId
reply({
type: "SELECTION_RESPONSE",
correlationId: msg.correlationId,
payload: selected,
});
break;
}
}, [selected]),
{ allowedOrigins: [PARENT_ORIGIN] }
);
// Notifica al padre que estamos listos
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>Esperando datos...</p>;
return (
<div style={{ padding: 16 }}>
<h3>Chart Widget</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>
);
}correlationId único a cada solicitud y hacer que el respondedor lo devuelva.crypto.randomUUID() para IDs. Está disponible en todos los navegadores modernos y es criptográficamente aleatorio.send es un callback estable (envuelto en useCallback con deps vacías) para que pueda pasarse como prop sin causar re-renders.messageTypes te permite limitar el handler de un componente solo a tipos de mensaje relevantes, lo que mantiene los handlers enfocados.message se activa para cada postMessage de cada iframe. Necesitas enrutar mensajes al handler correcto.event.source contra refs de iframe. Este es el enfoque más confiable.source o channel a tu protocolo de mensaje para que los handlers filtren por origen lógico.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;
// Solo maneja mensajes de ESTE 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 obtiene su propio handler con scope
function MultiIframePage() {
const chartRef = useRef<HTMLIFrameElement>(null);
const formRef = useRef<HTMLIFrameElement>(null);
useIframeMessage(chartRef, "https://charts.example.com", (data) => {
console.log("From chart:", data);
});
useIframeMessage(formRef, "https://forms.example.com", (data) => {
console.log("From form:", data);
});
return (
<>
<iframe ref={chartRef} src="https://charts.example.com/widget" title="Chart" />
<iframe ref={formRef} src="https://forms.example.com/widget" title="Form" />
</>
);
}El algoritmo de structured clone soporta más tipos que JSON pero aún tiene límites:
| Soportado | No Soportado |
|---|---|
| Primitivos (string, number, boolean, null, undefined) | Funciones |
| Objetos planos y arrays | Nodos DOM (Element, Document, etc.) |
Date | Symbol |
Map, Set | WeakMap, WeakSet |
RegExp | Instancias de clase (se pierde el prototipo) |
ArrayBuffer, typed arrays (Uint8Array, etc.) | Objetos Error (en algunos navegadores) |
Blob, File, FileList | Getters, setters, property descriptors |
ImageBitmap, ImageData | Objetos Proxy |
| Objetos anidados con referencias circulares | Closures |
Detalles clave:
class User { getName() {} } se convierte en un objeto plano al otro lado. Solo las propiedades propias enumerables sobreviven.JSON.stringify, structured clone puede serializar objetos con ciclos.ArrayBuffer se copia por defecto. Usa el parámetro transfer para transferir la propiedad (zero-copy) en su lugar. Ve la guía avanzada.Error tienen soporte de clonación inconsistente entre navegadores. Envía { message: error.message, stack: error.stack } en su lugar.useEffect, leerás valores obsoletos. Usa un ref para la función handler (como se muestra en el hook) o incluye el estado en el array de dependencias (lo que re-suscribe el listener en cada cambio).contentWindow es null hasta que el elemento iframe está en el DOM y ha comenzado a cargar. Siempre verifica null antes de llamar a postMessage.contentWindow aún existe pero ahora apunta al nuevo documento. Cualquier estado en el documento anterior se ha ido. Necesitas que la nueva página envíe un mensaje READY actualizado.postMessage lanza un DataCloneError. Esto puede ser sorprendente porque el error ocurre en el lado del remitente, no del receptor.message global puede recibir mensajes de iframes de extensión que no creaste. Siempre valida origen y forma del mensaje.| Enfoque | Cuándo Usar |
|---|---|
| MessageChannel | Puerto bidireccional dedicado para exactamente dos endpoints (sin broadcast, sin verificación de origen necesaria después de la configuración) |
| BroadcastChannel | Fan-out del mismo origen a todas las pestañas y ventanas (sin cross-origin) |
| Comlink (librería) | RPC de alto nivel sobre postMessage para workers e iframes; oculta el protocolo completamente |
Parámetros de URL de iframe src | Configuración inicial única (sin comunicación continua) |
| Estado compartido vía servidor | Cuando los iframes están en dominios diferentes y necesitan estado compartido persistente |
postMessage es fire-and-forget sin valor de retorno incorporado.correlationId único (vía crypto.randomUUID()) te permite emparejar una respuesta con la solicitud original.useRef, actualizado en cada render vía handlerRef.current = handler.useEffect lee del ref, por lo que siempre llama al handler más reciente sin re-suscribir el listener.export function usePostMessage<TIncoming = unknown>(
handler: (data: TIncoming, event: MessageEvent) => void,
options: { allowedOrigins: string[] }
) {
// TIncoming limita event.data dentro del handler
}event.source contra refs de iframe para identificar qué iframe envió el mensaje.source o channel a tu protocolo de mensaje para enrutamiento lógico.MessageChannel para crear un puerto dedicado por iframe (cubierto en la guía avanzada).class User { getName() {} } se convierte en un objeto plano en el lado receptor.postMessage(), no en el receptor.structuredClone() localmente para probar si tu payload es clonable.contentWindow aún existe pero apunta al nuevo documento.READY actualizado.const send = useCallback(
(target: Window, message: unknown, origin: string) => {
target.postMessage(message, origin);
},
[] // empty deps = referencia estable
);string[] para los nombres de tipos de mensaje permitidos.event.data?.type contra este array antes de invocar el handler.window.postMessage.message recibe mensajes de iframes de extensión.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 actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥