Search code examples
reactjsiframereact-dom

How to properly reference local document when rendering a React component in an iframe


I have a component that calls the document global. When I render this component in an iframe, it uses the current (parent) document as a reference, not the document of the iframe.

export const Modal: FC<ModalProps> = ({ opened, ...props }) => {
  // ...

  useEffect(() => {
    // Prevent background scrolling when the modal is opened.
    // This line locks scroll on the main DOM, but I only want it to be blocked on the iframe.
    document.body.style.overflow = opened ? "hidden" : "";
  }, [opened]); 

  // ...
}

Can I render the Modal component, so that document refers to the iframe document (and not the main DOM) ?

I use this custom component to render React elements in a separate iframe:

import { FC, DetailedHTMLProps, IframeHTMLAttributes, useEffect, useMemo, useState } from "react";

import { createPortal } from "react-dom";

export const IFrame: FC<DetailedHTMLProps<IframeHTMLAttributes<HTMLIFrameElement>, HTMLIFrameElement>> = ({ children, style, ...props }) => {
  const [contentRef, setContentRef] = useState<HTMLIFrameElement | null>(null);
  const headNode = useMemo(() => contentRef?.contentWindow?.document?.head, [contentRef]);
  const mountNode = useMemo(() => contentRef?.contentWindow?.document?.body, [contentRef]);

  // Setup head.
  useEffect(() => {
    if (headNode) {
      // Inherit parent styles (like compiled css modules or local fonts).
      for (const style of Array.from(document.head.querySelectorAll("link"))) {
        headNode.appendChild(style.cloneNode(true));
      }
      for (const style of Array.from(document.head.querySelectorAll("style"))) {
        headNode.appendChild(style.cloneNode(true));
      }
    }
  }, [headNode]);

  return (
    <>
      <iframe
        {...props}
        style={{ border: "none", ...style }}
        ref={setContentRef}
      />
      {mountNode ? createPortal(children, mountNode) : null}
    </>
  );
});

I also tried to create a new root and render the children inside, but it doesn't work either.

useEffect(() => {
  if (mountNode) {
    const root = createRoot(mountNode);
    root.render(children);

    return () => {
      root.unmount();
    };
  }
}, [mountNode]);

return (
  <iframe
    {...props}     
    style={{ border: "none", ...style }}
    ref={setContentRef}
  />
);

Solution

  • As no one else has had a go at this, please excuse a little speculation in this answer.

    What I think is happening here is that when you put content inside react’s <iframe> element it is creating your content using the srcdoc attribute, rather than the more normal src. This is then leading to your iframed content to exist in the same DOM context as your parent page.

    To fix this I think you either need to force React to load a new page into your iframe, or instead store the contextual data your after inside this component and pass a setState method to your child nodes.