Dynamic import of react hooks

Viewed 3891

Can we dynamically import hooks based on the value passed to the component?

For eg.

App.js

<BaseComponent isActive />

BaseComponent.js

if(props.isActive) {
  // import useActive (custom Hook)
}

I donot want these(custom hook) files to be imported and increase the size of BaseComponent even when the props contain falsey values.

3 Answers

You should extract the logic inside the useActive hook and dynamically import it instead of dynamically importing the hook since you should not call Hooks inside loops, conditions, or nested functions., checkout Rules of Hooks:

Let's say your useActive hook was trying to update the document title (in real world, it has to be a big chunk of code that you would consider using dynamic import)

It might be implemented as below:

// useActive.js

import { useEffect } from "react";    
export function useActive() {
  useEffect(() => {
    document.title = "(Active) Hello World!";
  }, []);
}

And you tried to use it in the BaseComponent:

// BaseComponent.js

function BaseComponent({ isActive }) {
  if (isActive) { // <-- call Hooks inside conditions ❌
    import("./useActive").then(({ useActive }) => {
      useActive();
    });
  }
  return <p>Base</p>;
}

Here you violated the rule "don't call Hooks inside conditions" and will get an Invalid hook call. error.

So instead of dynamically import the hook, you can extract the logic inside the hook and dynamically import it:

// updateTitle.js

export function updateTitle() {
  document.title = "(Active) Hello World!"
}

And you do the isActive check inside the hook:

// BaseComponent.js

function BaseComponent({ isActive }) {
  useEffect(() => {
    if (!isActive) return;

    import("./updateTitle").then(({ updateTitle }) => {
      updateTitle();
    });
  }, [isActive]);

  return <p>Base</p>;
}

It works fine without violating any rules of hooks.

I have attached a CodeSandbox for you to play around:

Edit hardcore-wing-p3uvc

To whoever encountered it as well: You can't use Hooks inside dynamically imported components(however, apparently if you does not use hooks even the first example works):

instead of:

const useDynamicDemoImport = (name) => {
  const [comp, setComp] = useState(null);
  useEffect(() => {
    let resolvedComp = false;
    import(`@site/src/demos/${name}`)
      .then((m) => {
        if (!resolvedComp) {
          resolvedComp = true;
          setComp(m.default);
        }
      })
      .catch(console.error);
    return () => {
      resolvedComp = true;
    };
  }, []);
  return comp;
};

const DemoPreviewer: FC<DemoPreviewerProps> = (props) => {
  comp = useDynamicDemoImport(props.name);
  return (
    <Paper sx={{ position: "relative" }}>
      {comp}
    </Paper>
  );
};

export default DemoPreviewer


use React Lazy instead and render the component later

const useDynamicDemoImport = (name) => {
  const Comp = React.lazy(() => import(`@site/src/demos/${name}`));
  return comp;
};

const RootDemoPreviewer: FC<DemoPreviewerProps> = (props) => {
  console.log("RootDemoPreviewer");
  return (
    <React.Suspense fallback={<div>Loading...</div>}>
      <DemoPreviewer {...props} />
    </React.Suspense>
  );
};

const DemoPreviewer: FC<DemoPreviewerProps> = (props) => {
  const Comp = useDynamicDemoImport(props.name);
  return (
    <Paper sx={{ position: "relative" }}>
      <Comp />
    </Paper>
  );
};

export default RootDemoPreviewer

Related