I'm a bit new to next.js, with typescript, version 13.5.5. The app must be placed inside an iframe, and I'm receiving some info from the parent/host via window. postmessage event. Im placing the listener in the useEffect of a client-side component imported at app/page.tsx. (I'm using a component so I can keep page.tsx ssr)
What is the problem? that the 'load' event the parent/host is using to trigger the iframe.postMessage() function is being fired before I can attach the listeners.
I currently can modify any of the applications, but I'm not sure which is the proper way to approach this, but I bet this must be a common iframe issue, but I'm not sure how to tackle it with a nextjs typescript app.
here of the code here: parent iframe:
import React, { useEffect, useState } from 'react';
const TheFrame = ({ appUrl, data}) => {
const [iframeElement, setIframeElement] = useState(undefined);
useEffect(() => {
if (data) {
postDataToIframe();
}
return () => {
if (!iframeElement) return;
iframeElement.removeEventListener('load');
};
}, []);
function postDataToIframe() {
const iframe = document.getElementById('theFrame');
const payload= { data: data};
if (iframe) {
setIframeElement(iframe);
iframe.addEventListener('load', () => {
setTimeout(() => {
const iframeDocument =
iframe.contentWindow ||
iframe.contentDocument ||
iframe.contentDocument;
iframeDocument.postMessage(payload, '*');
}, 100);
});
}
}
return (
<div>
<div
id="frame-container"
className="col col-md-12 col-lg-10 h-full col-lg-offset-1 text-center"
>
{data
&& (
<iframe
id="theFrame"
width="100%"
height="100%"
src={appUrl}
/>
)}
</div>
</div>
);
};
export default TheFrame;
Framed app
app/page.tsx
import { getEnvVariables } from "@/env-variables"
import { LandingContainer } from "./landing/landing-container"
const Landing = async () => {
const getAllowedDomains = async (): Promise<string[]> => {
const { allowedDomains } = await getEnvVariables()
return allowedDomains
}
return <LandingContainer allowedDomains={await getAllowedDomains()} />
}
export default Landing
and the landing container: app/landing/landing-container.tsx
"use client"
import Spinner from "@/components/Core/Loaders/Spinner"
import { useData } from "@/context/data-context"
import { RedirectType, redirect } from "next/navigation"
import { useEffect } from "react"
type Props = {
allowedDomains: string[]
}
export const LandingContainer = ({ allowedDomains }: Props) => {
const { data, setData } = useData()
/* eslint-disable */
useEffect(() => {
const listener = (event: MessageEvent): void => {
console.log(event)
if (
event.data &&
event.data.data&&
event.source &&
allowedDomains.includes(event.origin)
) {
console.log("data found")
setData(event.data.data)
}
}
window.addEventListener("message", listener, false)
console.log("listener open")
return () => {
window.removeEventListener("message", listener)
}
}, [])
/* eslint-enable */
useEffect(() => {
if (data) redirect("/home", RedirectType.replace)
}, [data])
return (
<div className="h-screen my-[40vh] flex flex-col self-center">
<Spinner />
</div>
)
}
notes: of course, data is not the name of the variables, I just placed a generic name for this example but the rest of the code is just the same.
I would appreciate any help.
After debating myself a bit, I got to a solution. Not optimal but appropriate when you have a short budget for this kind of issues.
having the framed app signaling that is ready to the parent app:
window.top.postMessage('reply', '*')
And this is how you listed in the parent:
window.onmessage = function(event){
if (event.data == 'reply') {
console('Reply received!');
}
};
and after that i post a message the same way, but I make sure my listener are ready.
credits: https://blog.logrocket.com/the-ultimate-guide-to-iframes/