Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript with React - use HOC on a generic component class

I have a generic React component, say like this one:

class Foo<T> extends React.Component<FooProps<T>, FooState> {
    constructor(props: FooProps<T>) {
        super(props);

    render() {
        return <p> The result is {SomeGenericFunction<T>()}</p>;
    }
}

I also have a HOC that looks similar to this one (but is less pointless):

export const withTd = 
    <T extends WithTdProps>(TableElement: React.ComponentType<T>): React.SFC<T> => 
(props: T) => <td><TableElement {...props}/></td>;

But when I use a component like this:

const FooWithTd = withTd(Foo);

There is no way to pass the type argument, as you can do neither withTd(Foo<T>), nor can you do FooWithTd, the type is always wrong. What is the proper way to do that?

EDIT: The problem is that I want to be able to have something like <FooWithTd<number> {...someprops}/> later on, as I don't know the desired type for T in the HOC.

like image 400
Joald Avatar asked Aug 23 '18 10:08

Joald


5 Answers

You can wrap your component which is created from a HOC into another component. It would look something like this:

class FooWithTd<T> extends React.Component<SomeType<T>> {
     private Container: React.Component<SomeType<T> & HOCResultType>; 

     constructor(props:SomeType<T>){
          super(props);
          this.Container = withTd(Foo<T>);
     }

     render() {
          return <this.Container {...this.props} />;
     }
}

Remember, you probably don't want the HOC inside your render function because it means that the component will be recreated every each render.

like image 112
habsq Avatar answered Oct 11 '22 19:10

habsq


Thanks for asking this question. I just figured out a way to specify a type parameter to a component after wrapping it with an HOC and I thought I'd share.

import React from 'react';
import withStyles from '@material-ui/core/styles/withStyles';
import { RemoveProps } from '../helpers/typings';

const styles = {
  // blah
};

interface Props<T> {
  classes: any;
  items: T[];
  getDisplayName: (t: T) => string;
  getKey: (t: T) => string;
  renderItem: (t: T) => React.ReactNode;
}

class GenericComponent<T> extends React.Component<Props<T>, State> {
  render() {
    const { classes, items, getKey, getDisplayName, renderItem } = this.props;

    return (
      <div className={classes.root}>
        {items.map(item => (
          <div className={classes.item} key={getKey(item)}>
            <div>{getDisplayName(item)}</div>
            <div>{renderItem(item)}</div>
          </div>
        ))}
      </div>
    );
  }
}

//   👇 create a `type` helper to that output the external props _after_ wrapping it
type ExternalProps<T> = RemoveProps<Props<T>, 'classes'>;
export default withStyles(
  styles
)(GenericComponent) as <T extends any>(props: ExternalProps<T>) => any;
//                       👆 cast the wrapped component as a function that takes
//                          in a type parameter so we can use that type
//                          parameter in `ExternalProps<T>`

The main idea is to cast the wrapped component as a function that takes in a type parameter (e.g. T) and use that type parameter to derive the external props after the component has been wrapped.

If you do this, then you can specify a type parameter when using the wrapped version of GenericComponent e.g.:

<GenericComponent<string> {/*...*/} />

Hopefully the code is explanatory enough for those who still have this problem. In general though, I consider this relatively advanced typescript usage and it's probably easier to use any instead of a generic parameter in the props

like image 24
Rico Kahler Avatar answered Oct 11 '22 18:10

Rico Kahler


Just stumbled upon this as well and thought I'd share what I came up with in the end.

Building on what @rico-kahler provided, my approach mapped to your code would be

export const FooWithTd = withTd(Foo) as <T>(props: FooProps<T>) => React.ReactElement<FooProps<T>>;

which you can then use like this

export class Bar extends React.Component<{}> {
  public render() {
    return (
      <FooWithTd<number> />
    );
  }
}

In my case, I have defaultProps as well and I inject props by ways of another HOC, the more complete solution would look like this:

type DefaultProps = "a" | "b";
type InjectedProps = "classes" | "theme";
type WithTdProps<T> = Omit<FooProps<T>, DefaultProps | InjectedProps> & Partial<FooProps<T> & { children: React.ReactNode }>;
export const FooWithTd = withTd(Foo) as <T>(props: WithTdProps<T>) => React.ReactElement<WithTdProps<T>>;
like image 42
Chris Avatar answered Oct 11 '22 20:10

Chris


Workaround: simple case

If your component's type parameter is used only for passing it to props, and users of the component do not expect it having any functionality beyond just passing props and rendering, you can explicitly hard-cast the result of your hoc(...args)(Component) to React's functional component type, like this:

import React, {ReactElement} from 'react';

class MyComponent<T> extends React.Component<MyProps<T>> { /*...*/ }

const kindaFixed = myHoc(...args)(MyComponent) as unknown as <T>(props: MyProps<T>) => ReactElement;

Workaround: more complex and with some runtime costs

You can use fabric-like function, supposed here:

class MyComponent<T> extends React.Component<MyProps<T>> { /*...*/ }

export default function MyComponentFabric<T>() {
    return hoc(...args)(MyComponent as new(props: MyProps<T>) => MyComponent<T>);
}

This one will require you to create new version of wrapped component for each type you use it with:

import MyComponentFabric from '...whenever';

const MyComponentSpecificToStrings = MyComponentFabric<string>();

It will allow you to access all public instance fields and methods of your component.

Summary

I faced this issue when tried to use connect from react-redux on my ExampleGenericComponent<T>. Unfortunatelly, it cannot be fixed properly until TypeScript will support HKT, and any HOC you use will update its typings respecting this feature.

There is possibly no correct solution (at least for now) for usages beyond just rendering, when you need to access component instance fields and methods. By 'correct' I mean 'without ugly explicit typecasts', and 'with no runtime cost'.

One thing you can try is to split your class-component into two components, one that will be used with HOC, and other that will provide fields and methods that you need.

like image 3
Nikita Ivanov Avatar answered Oct 11 '22 19:10

Nikita Ivanov


EDIT: After some changes to your code, it was only a wrong constraint T in your withTd function.

// I needed to change the constraint on T, but you may adapt with your own needs
export const withTd = <T extends FooProps<WithTdProps>>(
  TableElement: React.ComponentType<T>
): React.SFC<T> => (props: T) => (
  <td>
    <TableElement {...props} />
  </td>
)

// Explicitly typed constructor
// Removed after EDIT
//const FooW = Foo as new (props: FooProps<WithTdProps>) => Foo<WithTdProps>

// Inferred as React.StatelessComponent<FooProps<WithTdProps>>
const FooWithTd = withTd(Foo)

No longer relevant after EDIT :

You may find more information at this issue https://github.com/Microsoft/TypeScript/issues/3960

like image 1
Stouffi Avatar answered Oct 11 '22 20:10

Stouffi