Search code examples
reactjsiframereact-hooks

How to keep iframe mounted in React after it's parent unmounts


I have a react application that has components containing iframe. My components do get mounted and unmounted based on a state (e.g. {(showApp === true) && <MyApp/>}). Every time my component gets mounted, my iframe gets loaded again which do make some additional network calls and results in increased time. Is there a way I can memoize my components that can prevent iframe from loading again?

Here is a minimal reproducible example of my problem:

const App1 = () => {
  useEffect(() => {
    console.log("App1 mounted");
  }, []);

  return (
    <iframe
      width="100%"
      height="254"
      src="https://www.google.com/webhp?igu=1"
    ></iframe>
  );
};

const App2 = () => {
  useEffect(() => {
    console.log("App2 mounted");
  }, []);

  return (<div style={{ color: "red" }}>This is App2</div>);
};

const appMap = {
  1: <App1 />,
  2: <App2 />,
};

const MyApp = ({ app }) => {
  const Application = useMemo(() => appMap[app], [app]);

  return (
    <div>
      <span>This is MyApp Container:</span>
      {Application}
    </div>
  );
};

export default function App() {
  const [showApp, setShowApp] = useState(null);

  const App = useMemo(() => <MyApp app={showApp} />, [showApp]);

  return (
    <div>
      <div onClick={() => setShowApp("1")}>Show App 1</div>
      <div onClick={() => setShowApp("2")}>Show App 2</div>
      {showApp && App}
    </div>
  );
}

Solution

  • There’s no “memoization” involved. *What you want to do is to never unmount the component containing the iframe (or the iframe itself), you just want to hide it

    useMemo is way overused, and it’s not applicable here anyway

    You have 2 potential solutions:

    1. Don't unmount the mounted component that renders the iframe - hide it instead

    2. Use native dom methods to append the iframe when the component "containing it" is mounted and toggle the iframe's visibility when the component "containing it" is mounted/unmounted (so it's always in the DOM, just not always visible).

    Your current code structure lends itself towards solution 2. This is a rough example with comments to help illustrate the whats and whys. Your precise needs will likely be somewhat different:

    const IFRAME_ID = "MY_IFRAME_ID"
    const getIframe = () => document.getElementById(IFRAME_ID)
    const createIframe = (attrs) => {
       const iframe = document.createElement('iframe')
       iframe.id = IFRAME_ID;
       iframe.width = '100%';
       iframe.height = '254';
       iframe.src = 'https://www.google.com/webhp?igu=1'
       return iframe;
    }
    
    const App1 = () => {
      useEffect(() => {
         let iframe = getIframe()
         if(iframe) {
            // when this component mounts and the iframe exists, make sure it's visible
            iframe.style.display = 'block';
         } else {
            // when this component mounts and the iframe doesn't yet exist
            // create it and append it to the DOM
            iframe = createIframe(); 
            document.body.appendChild(iframe);
         }
    
         return () => {
            // when this component unmounts, hide the iframe
            iframe.style.display = 'none';
         }
      }, []);
    
      return null
    };