Home > Mobile >  How to type custom `preload` method for React.lazy components?
How to type custom `preload` method for React.lazy components?

Time:12-11

When trying to implement a preload() method for React.lazy() components, a typical pattern looks something like,

const ReactLazyPreload = (importStatement) => {
  const Component = React.lazy(importStatement);
  Component.preload = importStatement; // Property 'preload' does not exist on type 'LazyExoticComponent<T>'.
  return Component;
};

which can later be used, eg,

const MyComponent = ReactLazyPreload(() => import("./MyComponent.tsx");
const onHover = () => { MyComponent.preload() };

However, the assignment on the 3rd line of the first snippet causes a TS error,

Property 'preload' does not exist on type 'LazyExoticComponent<T>'.

I've been playing around with declare, but have been unsuccessful in removing the error. What type should be used for the preload() method?

CodePudding user response:

TypeScript is trying to prevent you from making a mistake here.

Just because other people follow a convention doesn't make it a good one: in this case, it's not a safe one. As a general rule, it's never safe to mutate things you don't own.

While I can't find anything in the React codebase at the current version tag (17.0.2) that would seem to cause an issue with assigning something to the preload property of a lazy component, that doesn't mean that the React maintainers won't use this property in a subsequent release. If that happens, and you overwrite that property, then unpredictable behavior would arise.

Instead of mutating the component, just return the preload function alongside it:

TS Playground link

import {default as React, lazy} from 'react';
import type {ComponentType, LazyExoticComponent} from 'react';

export type ReactLazyFactory<T = any> = () => Promise<{default: ComponentType<T>}>;

export type ComponentPreloadTuple<T = any> = [
  component: LazyExoticComponent<ComponentType<T>>,
  preloadFn: () => void,
];

export function getLazyComponentWithPreload <T = any>(componentPath: string): ComponentPreloadTuple<T>;
export function getLazyComponentWithPreload <T = any>(factory: ReactLazyFactory<T>): ComponentPreloadTuple<T>;
export function getLazyComponentWithPreload <T = any>(input: string | ReactLazyFactory<T>): ComponentPreloadTuple<T> {
  const factory = () => typeof input === 'string' ? import(input) : input();
  return [lazy(factory), factory];
}


// ----------
// Example.tsx

export type ExampleProps = {
  text: string;
};

export default function ExampleComponent ({text}: ExampleProps) {
  return <div>{text}</div>;
}


// ----------
// AnotherComponent.tsx

// use with path to component:
const [Example1, preloadExample1] = getLazyComponentWithPreload<ExampleProps>('./Example');

// use with factory function:
const [Example2, preloadExample2] = getLazyComponentWithPreload<ExampleProps>(() => import('./Example'));

CodePudding user response:

// extend lazy component with `preload` property
interface LazyPreload<Props>
  extends React.LazyExoticComponent<React.ComponentType<Props>> {
  preload: () => {};
}

function ReactLazyPreload<Props>(
  importStatement: () => Promise<{ default: React.ComponentType<Props> }>
) {
  // use Object.assign to set preload
  // otherwise it will complain that Component doesn't have preload
  const Component: LazyPreload<Props> = Object.assign(
    React.lazy(importStatement),
    {
      preload: importStatement,
    }
  );

  return Component;
}
  • Related