Search code examples
javascriptreactjsiframedom-eventsnext.js13

How to make Nextjs app listener to attach before window event 'load' its fired?


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 have tried adding the src of the iframe at last, with no success.
  • I also tried to add a setTimeOut on the parent, which worked, but I want to solve the race condition, not just produce the right winner.

I would appreciate any help.


Solution

  • 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/