Search code examples
javascriptcssreactjswebpackpreact

react - ensure css loaded before iframe renders


Dealing with a flash of unstyled content issue. I've got a chat widget that is injected onto users' pages. So that I don't clash with existing styles. I'm rendering in an iframe using react-frame-component. This works pretty nicely, but one problem I have is with the CSS.

react-frame-component accepts a prop head that you can use to pass a link to a stylesheet. Using MiniCSSExtractPlugin, I am able to get my css into a separate file that I can then link to:

render(props, state) {
  return (
    <Frame 
      head={<link rel="stylesheet" type="text/css" href="style.css" />}
      scrolling="no"
    >
      <ChatWidget />
    </Frame>
  );
}

With this, I get the dreaded flash of unstyled content. I think it's because everything including <ChatWidget> renders before the css can be fully downloaded.

Is there a way I can ensure the CSS is downloaded before everything renders?

Also, not sure if it matters, but I'm using Preact.


Solution

  • I ended up writing my own iframe component, using some tips from react-frame-component but writing it in a way I could be sure I had control over how it rendered. Here's what my iframe component looks like (note this uses Preact, which has some minor differences from React - it would be very easy to replicate this in React):

    import {h, Component, createRef} from 'preact';
    import {createPortal} from 'preact/compat';
    import {cssLink} from '../utilities';
    
    export default class Frame extends Component {
      iframeNode = createRef();
      iframeTest = document.createElement('iframe');
    
      static initialContent() {
        return `<!DOCTYPE html><head><link rel="stylesheet" type="text/css" href="${cssLink}"></head><body></body></html>`;
      }
    
      componentDidMount() {
        this.iframeNode.addEventListener('load', this.handleLoad);
      }
    
      handleLoad = () => {
        this.iframeRoot = this.iframeNode.contentDocument.body;
        this.forceUpdate();
      };
    
      sourceProp() {
        const canUseSrcDoc = 'srcdoc' in this.iframeTest;
    
        if (canUseSrcDoc) {
          return {
            srcdoc: Frame.initialContent(),
          };
        }
    
        // inserting Frame.initialContent() as the src is a hack for older browsers
        // to allow inserting our stylesheet before the children render, preventing
        // flashes of unstyled content
        return {
          src: `javascript: '${Frame.initialContent()}'`,
        };
      }
    
      render(props) {
        const {children, ...rest} = props;
    
        return (
          <iframe
            {...rest}
            {...this.sourceProp()}
            ref={ref => (this.iframeNode = ref)}
          >
            {this.iframeRoot && createPortal(children, this.iframeRoot)}
          </iframe>
        );
      }
    }