3

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.

priyanshu sinha
  • 595
  • 6
  • 14
  • Check out this thread: https://stackoverflow.com/questions/36367532/how-can-i-conditionally-import-an-es6-module#40277993 – Gh05d Jun 10 '20 at 08:17
  • Here I can see isActive is always true, so there's no point of importing it conditionally, And if the prop is not always true, so you must import useActive on the top level. – Siraj Alam Jun 10 '20 at 08:39

4 Answers4

3

You can dynamically import hooks as it is just a function (using require), but you shouldn't because you can't use hooks inside conditions.

See Rules of Hooks

Only call Hooks at the top level. Don’t call Hooks inside loops, conditions, or nested functions.

If you want conditionally use a hook, use the condition in its implementation (look for example at skip option of useQuery hook from Apollo GraphQL Client).

const useActive = (isUsed) => {
  if (isUsed) {
    // logic
  }
}
Dennis Vash
  • 50,196
  • 9
  • 100
  • 118
  • Will this not affect the size of the BaseComponent which did not require `useActive` at all is `props.isActive` is false? – priyanshu sinha Jun 10 '20 at 08:24
  • 2
    It will add a few **bytes** to the file size, but why you even consider it? Also, you don't have any other options as you restricted to React API, Please read: https://stackify.com/premature-optimization-evil/ – Dennis Vash Jun 10 '20 at 08:28
2

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

hangindev.com
  • 4,573
  • 12
  • 28
2

You could create a Higher Order Component that fetches the hook and then passes it down as a prop to a wrapped component. By doing so the wrapped component can use the hook without breaking the rules of hooks, eg from the wrapped component's point of view, the reference to the hook never changes and the hook gets called everytime the wrapped component renders. Here is what the code would look like:

export function withDynamicHook(hookName, importFunc, Component) {
    return (props) => {
        const [hook, setHook] = useState();

        useEffect(() => {
            importFunc().then((mod) => setHook(() => mod[hookName]));
        }, []);

        if (!hook) {
            return null;
        }

        const newProps = { ...props, [hookName]: hook };
        return <Component {...newProps} />;
    };
}

// example of a Component using it:
const MyComponent = ({useMyHook}) => {
    let something = useMyHook();
    console.log(something)
    return <div>myHook returned something, see the console to inspect it </div>
}
const MyComponentWithHook = withDynamicHook('useMyHook', () => import('module-containing-usemyhook'), MyComponent)


user3692823
  • 341
  • 1
  • 11
0

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

Eliav Louski
  • 3,593
  • 2
  • 28
  • 52