Using Typescript + React.lazy to load a dynamic list of components.
This last day I had to do a small but *extremely tedious* refactor on a page that loaded more than 20 different components, which every one of them had expense background calculations + api calls.
The problem appeared at the end of the development process, once all modules have been finished because we dont do premature optimizations ;)
My first problem was that I never had to use lazy loading before, and my knowledge about this topic was very limited. Good thing was that it was way easier than I initially thought it would be, this is a cheers to the React dev team.
To use React.lazy, you must import the component like
import ....
const Component1 = lazy(() => import('./Component1'))
I had to render the components based on an array of keys, so I had to make an object like this:
type ComponentsEnum = 'a' | 'b' | 'c'
const lazyModules: Record<ComponentsEnum, {
component: any,
props: any,
skip?: boolean}> = {
a: {
component: Component1, props: { foo: true }
}
}
So my second problem now was having to type the props....
After a lot of online search and active thinking.... Reverse mapping to the rescue!
"reverse mapping" is a concept where the inferred generic is being constructed backwards from the provided values. Once you do this, if you hover over the function you will see that the generic inferred type isn't actually appearing anywhere in the provided code, it's based on the code you did provide.
Recommended by LinkedIn
type NoInfer<T> = [T][T extends any ? 0 : never];
const createDict = <PropsDict extends Record<string, any>>(dict:
// (1)
[K in keyof PropsDict]: {
// (2)
component:
| ComponentType<PropsDict[K]>
| LazyExoticComponent<ComponentType<PropsDict[K]>>;
// (3)
props: NoInfer<PropsDict[K]>;
};
}) => dict;
What the hell does this even mean??
So how do you use this?
const Component1 = (props: { foo: true }) => null;
const Component2 = (props: { bar: false }) => null;
const lazyModules = createDict({
a: {
component: Component1,
props: { foo: true },
},
b: {
component: Component2,
props: { bar: false },
},
});
Everything in this code is getting inferred! Check this TS playground
One last bonus:
The downside of this code is that you do not know which components need to get rendered because inside the createDict function, the Record type is set as a string.
So how do we fix this? By currying createDict!
const createDictPartial = <Keys extends PropertyKey>() => <PropsDict extends Partial<Record<Keys, any>>>(dict:
[K in keyof PropsDict]: {
component:
| ComponentType<PropsDict[K]>
| LazyExoticComponent<ComponentType<PropsDict[K]>>;
props: NoInfer<PropsDict[K]>;
};
}) => dict;
const lazyModules = createDict<ComponentEnum>()({
a: {
component: Component1,
props: { foo: true },
},
})
Hope you all found this info useful!