Aelita
Aelita

Reputation: 79

React Typescript HOC TypeError when wrapping a Lazy component

Code:

Link to TypeScript Playground

React version:

"dependencies": {
  "react": "^18.1.0",
  "react-dom": "^18.1.0",
},
"devDependencies": {
  "@types/node": "^17.0.30",
  "@types/react": "^18.0.8",
  "@types/react-dom": "^18.0.3",
  "typescript": "^4.6.4",
}

I'm trying to wrap the lazy component in <React.Suspense> by a HOC to reduce redundant code, but I get a TS Error:

Type 'P' is not assignable to type 'IntrinsicAttributes & (T extends MemoExoticComponent<infer U> | LazyExoticComponent<infer U> ? ReactManagedAttributes<U, ComponentPropsWithRef<T>> : ReactManagedAttributes<...>)'

My HOC code is here:

function lazyLoading<P = {}, T extends ComponentType<P> = ComponentType<P>>(LazyComponent: LazyExoticComponent<T>) {
  return (props: P): JSX.Element => (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent {...props} />
    </Suspense>
  )
}

A simpler version without considering about props:

function lazyLoadingNoProps<T extends ComponentType<any>>(LazyComponent: LazyExoticComponent<T>) {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  )
}

Since I don't want to use LazyExoticComponent<any> (to get rid of any ), I decided to let TS infer the type of lazy component for me.
I've had a look at React.lazy's signature which works perfectly well, I decided to use it on my HOC

export function lazy<T extends ComponentType<any>>(
  factory: () => Promise<{default: T}>
): LazyExoticComponent<T>

And I get the following TS error for this simpler version:

Type '{}' is not assignable to type 'T extends MemoExoticComponent<infer U> | LazyExoticComponent<infer U> ? ReactManagedAttributes<U, ComponentPropsWithRef<T>> : ReactManagedAttributes<...>'

I've totally no idea what's happening here, as the Application is working as expected at runtime, but a TS error here in IDE.


Usage Example (simpler version without props):

const Admin = lazyLoading(lazy(() => import('./pages/Admin')))

const App = (): JSX.Element => (
  <Routes>
    <Route
      path="/admin"
      element={Admin}
    />
  {/* ... */}
)

HOC code is here: Link to TypeScript Playground

Upvotes: 2

Views: 2020

Answers (2)

export function withSuspense<P extends JSX.IntrinsicAttributes>(
    Component: React.ComponentType,
    suspenseProps: SuspenseProps & {
        FallbackComponent?: ComponentType;
    },
) {
    return function WithSuspense(props: P) {
        return (
            <Suspense
                fallback={
                    suspenseProps.fallback ||
                    (suspenseProps.FallbackComponent && createElement(suspenseProps.FallbackComponent))
                }
            >
                <Component {...props} />
            </Suspense>
        );
    };
}

Upvotes: -1

Elvis Duru
Elvis Duru

Reputation: 11

Here's how to achieve it in TypeScript

import { Loader } from "../components/loader";
import { Suspense } from "react";

/**
 * HOC to wrap a component in a Suspense component.
 */

export default function withSuspense<P>(Component: React.ComponentType & any) {
  return function WithSuspense(props: P) {
    return (
      <Suspense fallback={<Loader />}>
        <Component {...props} />
      </Suspense>
    );
  };
}

Upvotes: 0

Related Questions