I'm using a few third-party React hook libraries that aren't required for the initial render. E.g. react-use-gesture
, react-spring
, and react-hook-form
. They all provide interactivity, which can wait until after the UI is rendered. I want to dynamically load these using Webpack's codesplitting (i.e. import()
) after I render my component.
However, I can't stub out a React hook because it's essentially a conditional hook, which React doesn't support.
The 2 solutions that I can think of are:
Both solutions seem hacky and it's likely that future engineers will mess it up. Are there better solutions for this?
In React, dynamically importing a component is easy—you invoke React. lazy with the standard dynamic import syntax and specify a fallback UI. When the component renders for the first time, React will load that module and swap it in.
Introduction. Loading components dynamically is a technique that can replace writing import for many components. Rather than declaring every possible component that can be used, you can use a dynamic value for the path of a component.
lazy() is a function that enables you to render a dynamic import as a regular component. Dynamic imports are a way of code-splitting, which is central to lazy loading. A core feature as of React 16.6, React. lazy() eliminates the need to use a third-party library such as react-loadable .
You will need React if you are rendering JSX . To avoid that eslint warning, you should use react-in-jsx-scope rule from eslint-plugin-react. In that rule, it also explains why you need React in the file, even if you don't use it (you think you don't use it, but if you render JSX , you do).
As you say it, there are two ways to go about using lazy loaded hooks:
Something along the lines of
let lib
const loadLib = () => {...}
const Component = () => {
const {...hooks} = lib
...
}
const Parent = () => {
const [loaded, setLoaded] = useState(false)
useEffect(() => loadComponent().then(() => setLoaded(true)), [])
return loaded && <Component/>
}
This method is indeed a little hacky and a lot of manual work for each library
This can be streamlined with the help of React.Suspense
<Suspense fallback={"Loading..."}>
<ComponentWithLazyHook/>
</Suspense>
Suspense works similar to Error Boundary like follows:
This way is likely to get more popular when Suspense for Data Fetching matures from experimental phase.
But for our purposes of loading a library once, and likely caching the result, a simple implementation of data fetching can do the trick
const cache = {}
const errorsCache = {}
// <Suspense> catches the thrown promise
// and rerenders children when promise resolves
export const useSuspense = (importPromise, cacheKey) => {
const cachedModule = cache[cacheKey]
// already loaded previously
if (cachedModule) return cachedModule
//prevents import() loop on failed imports
if (errorsCache[cacheKey]) throw errorsCache[cacheKey]
// gets caught by Suspense
throw importPromise
.then((mod) => (cache[cacheKey] = mod))
.catch((err) => {
errorsCache[cacheKey] = err
})
};
const SuspendedComp = () => {
const { useForm } = useSuspense(import("react-hook-form"), "react-hook-form")
const { register, handleSubmit, watch, errors } = useForm()
...
}
...
<Suspense fallback={null}>
<SuspendedComp/>
</Suspense>
You can see a sample implementation here.
Edit:
As I was writing the example in codesandbox, it completely escaped me that dependency resolution will behave differently than locally in webpack.
Webpack import()
can't handle completely dynamic paths like import(importPath)
. It must have import('react-hook-form')
somewhere statically, to create a chunk at build time.
So we must write import('react-hook-form')
ourselves and also provide the importPath = 'react-hook-form'
to use as a cache key.
I updated the codesanbox example to one that works with webpack, the old example, which won't work locally, can be found here
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With