Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
// Padre envía un mensaje a un iframe
const iframeRef = useRef<HTMLIFrameElement>(null);
function sendToIframe(data: unknown) {
iframeRef.current?.contentWindow?.postMessage(
data,
"https://widget.example.com" // SIEMPRE especifica el origen destino
);
}
// Iframe (o cualquier ventana) escucha mensajes
useEffect(() => {
function handleMessage(event: MessageEvent) {
// CRÍTICO: siempre valida el origen
if (event.origin !== "https://parent.example.com") return;
console.log("Received:", event.data);
}
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, []);Cuándo usarlo: Cuando necesitas comunicación entre una página padre e un iframe incrustado, entre ventanas abiertas con window.open, o entre dos contextos de navegación que no pueden compartir directamente el scope de JavaScript. Los casos de uso comunes incluyen micro-frontends, widgets de terceros incrustados, formularios de pago, popups de OAuth, y flujos de autenticación entre orígenes.
Una página padre envía un objeto de tema a un iframe incrustado. El iframe aplica el tema y envía un reconocimiento.
import { useEffect, useRef, useState } from "react";
// -- Protocolo de mensajes tipado usando uniones discriminadas --
type ParentMessage =
| { type: "THEME_UPDATE"; payload: Theme }
| { type: "PING" };
type IframeMessage =
| { type: "THEME_ACK"; payload: { appliedAt: number } }
| { type: "PONG" };
interface Theme {
mode: "light" | "dark";
primaryColor: string;
fontSize: number;
}
const IFRAME_ORIGIN = "https://widget.example.com";
function ParentApp() {
const iframeRef = useRef<HTMLIFrameElement>(null);
const [acked, setAcked] = useState(false);
// Escucha respuestas del iframe
useEffect(() => {
function handleMessage(event: MessageEvent<IframeMessage>) {
if (event.origin !== IFRAME_ORIGIN) return;
switch (event.data.type) {
case "THEME_ACK":
console.log("Theme applied at:", event.data.payload.appliedAt);
setAcked(true);
break;
case "PONG":
console.log("Iframe is alive");
break;
}
}
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, []);
function sendTheme(mode: "light" | "dark") {
const message: ParentMessage = {
type: "THEME_UPDATE",
payload: { mode, primaryColor: "#3b82f6", fontSize: 16 },
};
iframeRef.current?.contentWindow?.postMessage(message, IFRAME_ORIGIN);
setAcked(false);
}
return (
<div>
<h1>Aplicación Padre</h1>
<button onClick={() => sendTheme("light")}>Tema Claro</button>
<button onClick={() => sendTheme("dark")}>Tema Oscuro</button>
{acked && <p>Widget reconoció el tema</p>}
<iframe
ref={iframeRef}
src={`${IFRAME_ORIGIN}/widget`}
title="Widget Incrustado"
style={{ width: 600, height: 400, border: "1px solid #ccc" }}
/>
</div>
);
}import { useEffect, useState } from "react";
interface Theme {
mode: "light" | "dark";
primaryColor: string;
fontSize: number;
}
type ParentMessage =
| { type: "THEME_UPDATE"; payload: Theme }
| { type: "PING" };
type IframeMessage =
| { type: "THEME_ACK"; payload: { appliedAt: number } }
| { type: "PONG" };
const PARENT_ORIGIN = "https://parent.example.com";
function Widget() {
const [theme, setTheme] = useState<Theme>({
mode: "light",
primaryColor: "#3b82f6",
fontSize: 16,
});
useEffect(() => {
function handleMessage(event: MessageEvent<ParentMessage>) {
// CRÍTICO: valida el origen antes de procesar
if (event.origin !== PARENT_ORIGIN) return;
switch (event.data.type) {
case "THEME_UPDATE":
setTheme(event.data.payload);
// Envía reconocimiento de vuelta al padre
const ack: IframeMessage = {
type: "THEME_ACK",
payload: { appliedAt: Date.now() },
};
// event.source es la ventana que envió el mensaje
(event.source as Window).postMessage(ack, event.origin);
break;
case "PING":
(event.source as Window).postMessage(
{ type: "PONG" } satisfies IframeMessage,
event.origin
);
break;
}
}
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, []);
return (
<div
style={{
backgroundColor: theme.mode === "dark" ? "#1e293b" : "#ffffff",
color: theme.mode === "dark" ? "#f1f5f9" : "#0f172a",
fontSize: theme.fontSize,
padding: 24,
minHeight: "100vh",
}}
>
<h2 style={{ color: theme.primaryColor }}>Contenido del Widget</h2>
<p>Tema actual: {theme.mode}</p>
</div>
);
}window.postMessage() es la API estándar del navegador para comunicación segura entre orígenes entre contextos de navegación (ventanas, iframes, popups, workers).targetWindow.postMessage(message, targetOrigin, transfer?).Cada manejador de mensajes recibe un MessageEvent con estas propiedades clave:
| Propiedad | Tipo | Descripción |
|---|---|---|
data | any | El payload del mensaje (copia estructurada de lo que se envió) |
origin | string | El origen de la ventana remitente (ej., https://example.com) |
source | Window o MessagePort o ServiceWorker o null | Referencia a la ventana remitente; úsala para responder |
ports | ReadonlyArray<MessagePort> | Puertos de MessageChannel transferidos con el mensaje |
lastEventId | string | Cadena vacía para postMessage (usada por SSE) |
postMessage(data, targetOrigin) en el objeto ventana destino (no en la suya propia).targetOrigin. Si no coincide, el mensaje se descarta silenciosamente.Date, Map, Set, ArrayBuffer, Blob, File, RegExp, arrays tipados y objetos anidados. No admite funciones, nodos DOM, Symbol, o WeakMap/WeakSet."message" de la ventana receptora se activa con un MessageEvent que contiene los datos clonados, el origen del remitente, y una referencia a la ventana del remitente.event.origin en cada manejador de mensajes. Sin esta comprobación, cualquier página que pueda hacer iframe de tu página (o de la que tu página haga iframe) puede enviar mensajes arbitrarios a tu manejador.targetOrigin cuando llames a postMessage. Usar "*" significa que cualquier origen puede recibir el mensaje. Esto es peligroso cuando el mensaje contiene datos sensibles (tokens, información del usuario, etc.). Consulta el documento de seguridad para detalles completos.// Patrón useEffect limpio para listeners de postMessage
useEffect(() => {
const ALLOWED_ORIGINS = new Set([
"https://widget-a.example.com",
"https://widget-b.example.com",
]);
function handleMessage(event: MessageEvent) {
if (!ALLOWED_ORIGINS.has(event.origin)) return;
// Acotar el tipo basado en unión discriminada
const msg = event.data;
if (typeof msg !== "object" || msg === null || !("type" in msg)) return;
switch (msg.type) {
case "THEME_UPDATE":
// handle...
break;
// ...
}
}
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, []); // Deps vacías: listener es estable, no se necesitan closures obsoletos// Define todos los mensajes en una unión. El campo "type" es el discriminante.
type AppMessage =
| { type: "NAVIGATE"; payload: { path: string } }
| { type: "AUTH_TOKEN"; payload: { token: string; expiresAt: number } }
| { type: "RESIZE"; payload: { width: number; height: number } }
| { type: "READY" }; // no se necesita payload
// Type guard para validar datos entrantes desconocidos
function isAppMessage(data: unknown): data is AppMessage {
if (typeof data !== "object" || data === null) return false;
if (!("type" in data)) return false;
const d = data as { type: string };
return ["NAVIGATE", "AUTH_TOKEN", "RESIZE", "READY"].includes(d.type);
}
// Uso en manejador
function handleMessage(event: MessageEvent) {
if (event.origin !== EXPECTED_ORIGIN) return;
if (!isAppMessage(event.data)) return;
// TypeScript ahora se estrecha correctamente en cada caso
switch (event.data.type) {
case "AUTH_TOKEN":
// event.data.payload es { token: string; expiresAt: number }
setToken(event.data.payload.token);
break;
case "NAVIGATE":
// event.data.payload es { path: string }
router.push(event.data.payload.path);
break;
}
}event.origin, ni siquiera en desarrollo. Construye el hábito temprano."*" como origen destino se emite a cualquier origen. Úsalo solo para mensajes verdaderamente públicos y no sensibles (y ni siquiera entonces, piénsalo dos veces).postMessage no devuelve un valor. Si necesitas una respuesta, debes implementar un protocolo request/response con IDs de mensaje (cubierto en la guía intermedia).event.source puede ser null si la ventana remitente ha sido cerrada antes de que tu manejador se active. Siempre null-check antes de responder.contentWindow antes de que se active el evento load del iframe fallará silenciosamente. Usa el callback onLoad del iframe o espera un mensaje READY del iframe.type del mensaje para enrutar al manejador correcto.Date, Map, Set, etc., pero lanzará una excepción con funciones y nodos DOM. Si necesitas verificar qué es cloneable, usa structuredClone() localmente para probar.| Enfoque | Cuándo Usar |
|---|---|
| BroadcastChannel | Pestañas o ventanas del mismo origen que necesitan compartir estado (sin soporte para cross-origin) |
| Channel Messaging (MessageChannel) | Puerto bidireccional dedicado entre exactamente dos contextos (cubierto en la guía avanzada) |
| SharedWorker | Múltiples pestañas del mismo origen que comparten un único worker para coordinación de estado |
| Custom Events | Comunicación dentro del mismo documento (sin cross-origin, sin cross-frame) |
| Server-Sent Events o WebSocket | Cuando necesitas comunicación mediada por servidor entre clientes |
| URL fragment o query params | Paso de datos único a un iframe (sin comunicación continua) |
Date, Map, Set, ArrayBuffer, Blob, File, y RegExp.Symbol, WeakMap, y WeakSet no se admiten y lanzarán un DataCloneError.Set de orígenes conocidos y buenos para una validación rápida y de coincidencia exacta."*" para mensajes verdaderamente públicos y no sensibles.type AppMessage =
| { type: "NAVIGATE"; payload: { path: string } }
| { type: "READY" };
function handleMessage(event: MessageEvent<AppMessage>) {
if (event.origin !== EXPECTED_ORIGIN) return;
switch (event.data.type) {
case "NAVIGATE":
// event.data.payload es { path: string }
break;
}
}function isAppMessage(data: unknown): data is AppMessage {
if (typeof data !== "object" || data === null) return false;
if (!("type" in data)) return false;
const d = data as { type: string };
return ["NAVIGATE", "READY"].includes(d.type);
}contentWindow no está listo.onLoad del iframe o espera un mensaje READY del iframe antes de enviar.postMessage no devuelve un valor. Si necesitas una respuesta, debes construir un protocolo request/response con IDs de correlación.Set es una coincidencia de cadena exacta, que es tanto más segura como más rápida.Date, Map, Set, ArrayBuffer, Blob, referencias circulares, y más.Date se convierte en una cadena, Map se convierte en {}).JSON.stringify antes de enviar o JSON.parse al recibir.event.source es null.event.source antes de llamar a postMessage en él para responder.useEffect(() => {
function handleMessage(event: MessageEvent) {
if (event.origin !== EXPECTED) return;
// handle message
}
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, []);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: 10 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥