In React with material-ui I am trying to create a JSX component that accepts generic parameters and also uses the withStyles
HOC to inject my styles.
The first approach was like this:
const styles = (theme: Theme) => createStyles({
card: {
...
}
});
interface Props<T> {
prop: keyof T,
...
}
type PropsWithStyles<T> = Props<T> & WithStyles<typeof styles>;
export default withStyles(styles)(
class BaseFormCard<T> extends React.Component<PropsWithStyles<T>> {
...
}
),
But when trying to use this, the generic types are lost
<BaseFormCard<MyClass> prop={ /* no typings here */ } />
The only solution I could find was to wrap the export in a function which takes the generic parameter and constructs the component.
export default function WrappedBaseFormCard<T>(props: Props<T>): ReactElement<Props<T>> {
const wrapper = withStyles(styles)(
class BaseFormCard<T> extends React.Component<PropsWithStyles<T>> {
...
}
) as any;
return React.createElement(wrapper, props);
}
However this is ridiculously complicated and even comes with runtime cost, although it is only trying to solve problems typings.
There has to be a better way to use JSX components with generic parameters and HOCs.
This is closely related to the issue here https://github.com/mui-org/material-ui/issues/11921, but there was never satisfying solution and the issue is now closed.
The more I think about the question, the more I like Frank Li's approach. I'd make two modifications: (1) introduce an extra SFC to avoid a cast, and (2) grab the outer props type from the wrapped component C
instead of hard-coding it. (If we hard-coded Props<T>
, TypeScript would at least check that it is compatible with this.C
, but we are at risk of requiring props that this.C
doesn't actually require or failing to accept optional props that this.C
actually accepts.) It's jaw-dropping that referencing a property type from a type argument in the extends
clause works, but it seems to!
class WrappedBaseFormCard<T> extends React.Component<
// Or `PropsOf<WrappedBaseFormCard<T>["C"]>` from @material-ui/core if you don't mind the dependency.
WrappedBaseFormCard<T>["C"] extends React.ComponentType<infer P> ? P : never,
{}> {
private readonly C = withStyles(styles)(
// JSX.LibraryManagedAttributes handles defaultProps, etc. If you don't
// need that, you can use `BaseFormCard<T>["props"]` or hard-code the props type.
(props: JSX.LibraryManagedAttributes<typeof BaseFormCard, BaseFormCard<T>["props"]>) =>
<BaseFormCard<T> {...props} />);
render() {
return <this.C {...this.props} />;
}
}
I think any complaints about runtime overhead of this approach are probably nonsense in the context of a whole React application; I'll believe them when someone presents data supporting them.
Note that Lukas Zech's approach using an SFC is very different: every time the props to the outer SFC change and it is called again, withStyles
is called again, generating a wrapper
that looks to React like a whole new component type, so React throws away the old wrapper
instance and a new inner BaseFormCard
component gets created. This would have undesirable behavior (resetting state), not to mention greater runtime overhead. (I haven't actually tested this, so let me know if I'm missing something.)
This works enough for me using Visual Studio:
import React from "react";
import { withStyles, WithStyles } from "@material-ui/styles";
const styles = {
root: { ... },
};
export interface Props<T> { ... }
export interface State<T> { ... }
export class TableComponent<T> extends React.PureComponent<Props<T> & WithStyles<typeof styles>, State<T>> {
render () {
return <div className={this.props.classes.root} />;
}
}
export const Table = (withStyles(styles)(TableComponent) as any) as new <T>() => TableComponent<T>;
export default Table;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With