priyanshu sinha
priyanshu sinha

Reputation: 625

Dynamic import of react hooks

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.

Upvotes: 3

Views: 9068

Answers (4)

user3692823
user3692823

Reputation: 381

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)


Upvotes: 4

Eliav Louski
Eliav Louski

Reputation: 5274

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

Upvotes: 0

hangindev.com
hangindev.com

Reputation: 4873

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

Upvotes: 2

Dennis Vash
Dennis Vash

Reputation: 53894

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
  }
}

Upvotes: 3

Related Questions