For my project, I generate the content of a HTML landing page and then render it in an iFrame. Once it has finished loading, I allow my user to preview the page and then save and publish the generated content. This approach will also work with regular iFrame with a src attribute if you want to load and check a specific URL. In that case, you will need to remove srcDoc and sandbox attributes and use the src attribute instead.

IFrameRenderer Component

Let's create the IFrameRenderer component iframe-renderer.component.tsx. The IFrameRenderer component is a basic iFrame implementation. It accepts an iFrameRef created in a parent component, this is the ref we use to check the loaded status. The load event which we will listen to is fired when the page has loaded, including all dependent resources such as stylesheets and images.

// iframe-renderer.component.tsx
import type { RefObject, FC } from 'react';

export const IFrameRenderer: FC<{
  landingPageHtml: string;
  iframeRef?: RefObject<HTMLIFrameElement>;
}> = ({ landingPageHtml, iframeRef }): JSX.Element => {
  return (
    <iframe
      ref={iframeRef}
      srcDoc={landingPageHtml}
      title="iframe-preview"
      width="100%"
      height="700px"
      sandbox
    ></iframe>
  );
};

Note: The srcDoc attribute is usually used with the sandbox attribute. MDN Web Docs - The Inline Frame element.

An example parent component

Below is a simple example component that implements our newly created IFrameRenderer. We need a useState() hook to maintain a value of true or false for displaying if the iFrame has finished loading.

// Example parent component
import type { FC } from 'react';
import { useState } from 'react';
import { IFrameRenderer } from './iframe-renderer.component';

export const IFramePreviewPage: FC = (): JSX.Element => {
  const [isIFrameLoaded, setIsIFrameLoaded] = useState<boolean>(false);

  const landingPageHtml =
    '<p>This content is being injected into the iFrame.</p>';

  return (
    <div>
      <p>iFrame is loaded: {String(isIFrameLoaded)}</p>
      <IFrameRenderer landingPageHtml={landingPageHtml} />
    </div>
  );
};

In our parent component, we need to create a reference for the iFrame and pass it to our IFrameRenderer. Then we write a useEffect() hook to change the isIFrameLoaded state when the iFrame has finished loading. To detect when the iFrame has finished loading, we need to add an event listener to the iFrame, and we need to return the removeEventListener function to remove the event listener if our component unmounts.

import type { FC } from 'react';
import { useState } from 'react';
import { useState, useEffect, useRef } from 'react';
import { IFrameRenderer } from './iframe-renderer.component';

export const IFramePreviewPage: FC = (): JSX.Element => {
  const iFrameRef = useRef<HTMLIFrameElement>(null);
  const [isIFrameLoaded, setIsIFrameLoaded] = useState<boolean>(false);

  const iframeCurrent = iFrameRef.current;

  useEffect(() => {
    iframeCurrent?.addEventListener('load', () => setIFrameLoaded(true));

    return () => {
      iframeCurrent?.removeEventListener('load', () => setIFrameLoaded(true));
    };
  }, [iframeCurrent]);

  const landingPageHtml =
    '<p>This content is being injected into the iFrame.</p>';

  return (
    <div>
      <p>iFrame is loaded: {String(isIFrameLoaded)}</p>
      <IFrameRenderer landingPageHtml={landingPageHtml} />
      <IFrameRenderer landingPageHtml={landingPageHtml} iFrameRef={iFrameRef} />
    </div>
  );
};

Convert to a custom hook

We have a working example, but we can do better by writing a reusable custom hook. Create a file called use-is-iframe-loaded.hook.ts and convert the code above to a Custom Hook.

// use-is-iframe-loaded.hook.ts
import type { RefObject } from 'react';
import { useState, useEffect } from 'react';

export const useIsIFrameLoaded = (
  iframeRef: RefObject<HTMLIFrameElement>
): boolean => {
  const [isIFrameLoaded, setIsIFrameLoaded] = useState<boolean>(false);
  const iframeCurrent = iframeRef.current;

  useEffect(() => {
    iframeCurrent?.addEventListener('load', () => setIsIFrameLoaded(true));

    return () => {
      iframeCurrent?.removeEventListener('load', () => setIsIFrameLoaded(true));
    };
  }, [iframeCurrent]);

  return isIFrameLoaded;
};

Final code usage

Now we have our custom hook, we can simplify our earlier parent component example.

// Example usage with custom hook
import type { FC } from 'react';
import { useRef } from 'react';
import { IFrameRenderer } from './iframe-renderer.component';
import { useIsIFrameLoaded } from './use-is-iframe-loaded.hook';

export const IFramePreviewPage: FC = (): JSX.Element => {
  const iframeRef = useRef<HTMLIFrameElement>(null);
  const isIFrameLoaded = useIsIFrameLoaded(iframeRef);

  const landingPageHtml =
    '<p>This content is being injected into the iFrame.</p>';

  return (
    <div>
      <p>iFrame is loaded: {String(isIFrameLoaded)}</p>
      <IFrameRenderer landingPageHtml={landingPageHtml} iFrameRef={iFrameRef} />
    </div>
  );
};

That's a Wrap!

Now we have a custom hook that we can use for future projects, and we can also see a typical pattern for implementing custom hooks with different types of event listeners. If you found this tutorial useful, please drop a comment below.