1

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?

Rohan Agarwal
  • 2,441
  • 2
  • 18
  • 35

1 Answers1

3

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;
};
cbr
  • 12,563
  • 3
  • 38
  • 63
  • My use case is exactly what you have defined as the second part,i.e., changing value of loader. However, my code just works fine without passing it as a prop but passing it as an argument. Any thoughts on it? – Rohan Agarwal Jul 09 '20 at 12:41
  • 1
    @RohanAgarwal It does work, but you'll run into the issues I described in the second paragraph. Try for example wrapping a component with state. Then change that state inside the component (e.g. with a button and onClick), then change the value of `loading`. See what happens to the state. – cbr Jul 09 '20 at 12:44
  • Or maybe because I don't have any state inside my HOC, I am not facing the issue. Is my understanding correct? Right now, in my functional component, just before my return method, i am creating the enhanced component and using it inside my return method. the loading argument is getting updated via state changes. – Rohan Agarwal Jul 09 '20 at 12:49
  • 1
    @RohanAgarwal It's an antipattern to create a component inside the render function (i.e. render() or a function component). Try out the example here: https://codesandbox.io/s/xenodochial-cannon-b1jww?file=/src/App.tsx - Click on "add" a few times, then toggle loading. – cbr Jul 09 '20 at 12:51
  • TYSM. This is by far one of the best discussions that I had on SO :) If you could help me on one more scenario. Suppose, I am controlling my loading value not through a state but by a selector (redux). Now, if instead of passing a boolean value as a prop to my HOC, can I pass my selector name (a string) to my HOC and inside the HOC I listen to the selector and control my loading class. Is there any problem with this approach? – Rohan Agarwal Jul 09 '20 at 14:33
  • 1
    That works. You could even pass the selector function itself, kinda like how `connect` takes in mapStateToProps etc – cbr Jul 09 '20 at 15:11