React: change the component tree without unmounting/remounting certain components?

I have a wrapper component that’s dynamically loaded. While it’s loading, it’s just a div. I dynamically loaded it because it uses large libraries that I don’t want to include with the initial load.

E.g. the component tree looks like:

<WrapperDeferred>
  <Child />
</WrapperDeferred>

WrapperDeferred looks something like:

let Wrapper;

function WrapperDeferred({ children }) {
  const forceUpdate = useForceUpdate();

  useEffect(() => {
    if (!Wrapper) {
      import('./Wrapper').then(module => {
        Wrapper = module.default;
        forceUpdate();
      });
    }
  }, []);

  return Wrapper ? <Wrapper>{children}</Wrapper> : <div>{children}</div>;
}

The issue is that while Wrapper is loading, the child component renders and mounts. Then, when Wrapper loads, the child component unmounts, rerenders, and remounts. Is there a way to reuse the existing child component?

21 thoughts on “React: change the component tree without unmounting/remounting certain components?”

  1. You can do something like this with Webpack lazy-loading (†):

    import * as React from "react";
    import PropTypes from "prop-types";
    
    const DynamicImport = ({ children, loadFile }) => {
      const isMounted = React.useRef();
      const [state, setState] = React.useState({ Component: null });
      const { Component } = state;
    
      const importFile = React.useCallback(async () => {
        try {
          const { default: file } = await loadFile();
    
          if (isMounted && isMounted.current) setState({ Component: file });
        } catch (err) {
          console.error(err.toString());
        }
      }, [loadFile]);
    
      React.useEffect(() => {
        importFile();
      }, [importFile]);
    
      return (
        <span ref={isMounted}>
          {Component ? <Component>{children}</Component> : <div>{children}</div>}
        </span>
      );
    };
    
    DynamicImport.propTypes = {
      loadFile: PropTypes.func.isRequired,
      children: PropTypes.node
    };
    
    export default DynamicImport;
    

    Then use it like so:

    import * as React from "react";
    import DynamicImport from "./components/DynamicImport";
    
    const Example = () => (
      <DynamicImport
        loadFile={() =>
          import(
            /* webpackMode: "lazy" */
            /* webpackChunkName: "large-component" */
            "./components/LargeComponent"
          )
        }
      >
        Hello world
      </DynamicImport>
    );
    
    export default Example;
    

    Working Example:

    Edit Dynamic Lazy Loading

    Demo: https://bo8p7.csb.app/


    The major downside to this approach is that Webpack won’t be able to tree-shake the component’s dependencies. As a result, the entire 3rd party library/libraries (and ALL of their dependencies) will be added to the production bundle. Check this similar question for a more in depth explanation.

    Reply

Leave a Comment