javascriptreactjstypescriptreact-hoc

Argument of type '(props: ITableProps) => JSX.Element' is not assignable to parameter of type ... - React HOC


I have a loader HOC

HOC:

const withLoader = <P extends object>(WrappedComponent: new () => React.Component<P, any>, loading: boolean) => {

    return class WithLoading extends React.Component<P, any> {
        render() {

            return (
                <div >
                    <div className={`${loading} ? 'loader' : '' "></div>
                    <WrappedComponent {...this.props} /> 
                </div>
            )
        }
    }
}

I am using this in this way, for example:

const TableHOC = withLoader(Table,true);

Now my table or any other component for example, will have its own well defined props interface. Everything is well typed.

However I am getting this issue

Argument of type '(props: ITableProps) => JSX.Element' is not assignable to parameter of type 'new () => Component<object, any, any>'.
  Type '(props: ITableProps) => Element' provides no match for the signature 'new (): Component<object, any, any>'.ts(2345)

How can I solve this?


Solution

  • You'll want to use React.ComponentType for the type instead:

    const withLoader = <P extends object>(WrappedComponent: React.ComponentType<P>, loading: boolean) => {
      // ...
    }
    

    Just a note though, if you're planning on toggling the value of loading via a state change, you'll want to pass it as a prop instead. This is because every time you call withLoader to get an enhanced component out, it is a new component, meaning that if you do it inside render, React will always unmount and remount that rendered component. This also means that any state inside the enhanced component will be lost.

    For example:

    interface WithLoadingProps {
      loading: boolean;
    }
    
    const withLoader = <P extends object>(
      WrappedComponent: React.ComponentType<P>
    ) => {
      return class WithLoading extends React.Component<P & WithLoadingProps, any> {
        render() {
          const { loading } = this.props;
          return (
            <div>
              <div className={loading ? "loader" : ""}>
                <WrappedComponent {...this.props} />
              </div>
            </div>
          );
        }
      };
    };
    

    Sample use:

    const MyComponent = ({ text }: { text: string }) => {
      return <div>{text}</div>;
    };
    const MyLoadingComponent = withLoader(MyComponent);
    
    class Foo extends React.Component<{}, { loading: boolean }> {
      render() {
        const { loading } = this.state;
        return <MyLoadingComponent loading={loading} text="foo" />;
      }
    }
    

    As a cherry on top, consider also adding the displayName as instructed in React's documentation - this will enhance your debugging experience when working with React's devtools.

    With:

    enter image description here

    Without:

    enter image description here

    interface WithLoadingProps {
      loading: boolean;
    }
    
    const getDisplayName = <P extends object>(Component: React.ComponentType<P>) =>
      Component.displayName || Component.name || "Component";
    
    const withLoader = <P extends object>(WrappedComponent: React.ComponentType<P>) => {
      class WithLoading extends React.Component<P & WithLoadingProps, any> {
        static readonly displayName = `WithLoading(${getDisplayName(WrappedComponent)})`;
    
        render() {
          const { loading } = this.props;
          return (
            <div>
              <div className={loading ? "loader" : ""}>
                <WrappedComponent {...this.props} />
              </div>
            </div>
          );
        }
      }
    
      return WithLoading;
    };