The react-loadable library provides an excellent way to load React components on demand while displaying a temporary view until they finish loading (such as a spinner animation). Recently, I tried using react-loadable with TypeScript, and the compiler reported a strange and unexpected error. Here’s the journey I took figure out what was wrong, and how I fixed it.
You can see that my code below is very simple, but I was getting an error, for some reason:
import * as React from "react";
import * as Loadable from "react-loadable";
import MyLoadingComponent from "./MyLoadingComponent";
const LoadableMyComponent = Loadable(
{
loader: () => import("./MyComponent"),
loading: MyLoadingComponent
});
The full compiler error appears next (with a modified file path to make it more readable):
Argument of type '{ loader: () => Promise<typeof "/path/to/MyComponent">' is not assignable to parameter of type 'Options<any, typeof "/path/to/MyComponent">'. Type '{ loader: () => Promise<typeof "/path/to/MyComponent">' is not assignable to type 'OptionsWithRender<any, typeof "/path/to/MyComponent">'. Property 'render' is missing in type '{ loader: () => Promise<typeof "/path/to/MyComponent">'.
In my editor, the error begins at the opening curly brace {
and ends at the closing curly brace }
, so it’s clear that there’s something wrong with the options object that I’m passing in. However, it was not immediately obvious to me why the TypeScript compiler didn’t like my object.
The error says that a render
property is missing. My first thought was that I was missing a render()
method in a component somewhere. However, both MyComponent
and MyLoadingComponent
are React components with proper render()
methods defined.
The code for MyComponent.tsx
appears below:
import * as React from "react";
export default class MyComponent extends React.Component<any>
{
render()
{
return <div>MyComponent has loaded</div>
}
}
And here’s the code for MyLoadingComponent.tsx
:
import * as React from "react";
export default class MyLoadingComponent extends React.Component<any>
{
render()
{
return <div>Loading...</div>;
}
}
As you can see, render()
is not missing in my components. So where exactly should a property named render
be defined?
After reading through the react-loadable documentation, I figured out that the options object can be one of a few different variations, and one of them requires a render
property. The TypeScript compiler thinks I’m trying to use the variation where this property is required…
…but I’m not.
According to the following comment from the type definitions, I need to meet the following requirements to make the render
property optional:
If you do not want to provide a render function, ensure that your function is returning a promise for a React.ComponentType or is the result of import()ing a module that has a component as its `default` export.
You can see that the function that I pass to the loader
property should work. I’m loading a module containing MyComponent
as the default export, and it extends React.Component
.
loader: () => import("./MyComponent")
After some trial and error, I determined the real problem. The React component that I pass to the other option (loading
) isn’t accepted because I typed its “props” as any
:
class MyLoadingComponent extends React.Component<any>
According to the type definitions for react-loadable, I should use ReactComponent.<Loadable.LoadingComponentProps>
instead. I would have expected the TypeScript compiler to figure out that Loadable.LoadingComponentProps
can be assigned to any
, but I guess not.
Anyway, here’s how I should have defined MyLoadingComponent
instead:
import * as React from "react";
import * as Loadable from "react-loadable";
export default class MyLoadingComponent extends React.Component<Loadable.LoadingComponentProps>
{
render()
{
return <div>Loading...</div>;
}
}
It took me quite some time to figure out what exactly was wrong with my code. The TypeScript compiler was fixated on that render
property, which led me down the wrong path. I wish the compiler would have realized that my React.Component
was using type any
for its props instead of the Loadable.LoadingComponentProps
. I wonder if there’s a better way to define these options in the type definitions so that the wrong type for props would have taken precedence over the missing (but not really) render
property?
Hi Josh!
Seems here you just need to use exact type assertion.
Like that:
Please take note of “as Promise” piece.
I suppose it happens because of something wrong with tsconfig. Seems we should use some lib in it for typescript work with async import in a right way.
Anyway, thanks for your article Josh
Thanks for the idea! However, I think that the compiler already knows that import() returns a Promise, even without the cast. You can see that it was successfully inferred in the error message: “loader: () => Promise”