Lazy Loading React Components Without Sacrificing Type Safety
Learn how to lazy load React components while maintaining strong type safety using a simple utility function.
Over the years, the React community has found many creative ways to dynamically import components and improve a React app's performance by lazy-loading them on demand. This has led to the creation of many well-known libraries, but sometimes, adding a new library to the codebase implies maintaining version updates and potential breaking changes.
Once upon a time... React.lazy() and Suspense
In 2018, the React core team introduced the React.lazy()
function that allows, combined with the <Suspense />
component, to enable code-splitting in your app without relying on external libraries. This was a great addition, and every React developer is familiar with the following snippet.
This was a great addition to the community, although our expectations grew with the adoption of this new feature and, as a devs community, we demanded more features.
Does this work with Typescript?
React.lazy()
provides out-of-the-box support for component type inference.
Now, there is no issue at all with this code, and it's probably one of the most common approaches taken when it comes to lazy-load components.
However, while working on a big codebase that requires precise code-splitting to keep performance high, we might find ourselves writing a lot of times something like this:
This can get easily out of hand and you'll find your self having to every time wrap your component with a Suspense component, explicitly passing props type that could be automatically inferred, and just making everything less readable.
The dynamic utility solution
I call it dynamic
inspired by the next/dynamic
utility function, but since not every codebase is based on the Next.js framework, having a similar utility available in your codebase can be achieved quite easily.
It is a basic implementation, but it will take care of many aspects of a component lazy-loading:
- Ensure the dynamic import passes through the
React.lazy()
utility. - The lazy imported component is wrapped with
<Suspense />
- The component properties are automatically inferred using Typescript generics.
- The imported component can still receive a forwarded ref.
- We can define a fallback component to use while loading the async code chunk.
Breaking down the above implementation:
type Loader
defines what we expect as an argument, which should be a dynamic import call.interface DynamicOptions
defines the accepted optional parameters, currently only thefallback
item to display while loading.- The generic definition of
dynamic
inferring aReact.ComponentType
type from the passed loader function. React.lazy()
is then used to dynamically import the lazy component.- A new component is returned that wraps with
<Suspense />
(and applies thefallback
if passed) the original lazily loaded component. I usedReact.createElement()
instead of a JSX instantiation to explicitly pass theref
property along to the others and not incur in typing issues. - Use
React.ComponentPropsWithRef<TElement>
to extract the properties type from the imported component.
The final result allows us to use the dynamic
utils anywhere in our codebase, always retrieving strongly typed components and applying all the React lazy loading requirements.
I implemented this utility function for the Open Source Kibana codebase, and it's been a great experience using it so far!
Last updated: