Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create stateless functional components that accept generics?

Typescript generics can used to extend interfaces.

interface Sample1<P> {
  prop1: P;
}

interface Sample2<P> extends Sample1<P> {
  prop2: string;
}

But when I try to create a functional component that uses the generic interface, typescript throws error.

const SampleSFC: React.SFC<Sample2<P>> = () => <div />;

error TS2304: Cannot find name 'P'.

If I replace P with a known type like string the error goes away.

const SampleSFC: React.SFC<Sample2<string>> = () => <div />;

Instead of hard coding type of P, that totally kills the purpose of generics, I want to enable users of SampleSFC to set type of P.

How can I do that? If it's not possible then what alternate design I should follow that would let me have SFC with generic props.

like image 658
Andrew-Dufresne Avatar asked Dec 16 '17 18:12

Andrew-Dufresne


People also ask

Can stateless components have functions?

A stateless function component is a typical React component that is defined as a function that does not manage any state. There are no constructors needed, no classes to initialize, and no lifecycle hooks to worry about. These functions simply take props as an input and return JSX as an output.

Can functional components be stateful?

Every React component that has a state influences its behavior (/render) or another component's behavior can be considered as a "stateful component". So for the function in the question - yes, App is stateful.

Which method is used to make functional components stateful?

The setState() method rerenders the component, and you have a working stateful component.


1 Answers

Generic type variables can be used in function and type declarations like your interfaces Sample1 and Sample2. But when you actually use/call/invoke generic types, you have to specify a concrete type argument instead of P.

React.SFC - the stateless function component type - is an interface declaration, so using it in an const assignment requires a concrete type for P.

The only solution that comes to my mind is to make the SFC look like a function type for the ts-compiler, since function declarations/expressions can be assigned generic type parameters. See Typescript docs for some examples.

Generic SFC declaration

Leave out the React.SFC type annotation to your assignment const SampleSFC:

const SampleSFC = <P extends {}>(props: Sample2<P>) => <div>{props.prop1} </div>;

Note: The <P extends {}> part is needed due to JSX/TSX compatibility issues with generics, see the end of this post.

This way, your component remains a plain function. If you need the additional members from React.SFC, add them to your props. E.g. if children needed:

const SampleSFC = <P extends {}>(props: Sample2<P> & { children?: ReactNode })
 => <div >{props.prop1} </div>;

Generic SFC usage

Since Typescript 2.9, the type parameter can be directly set in JSX.

const MyApp: React.SFC<{}> = (props) => {
    return <SampleSFC<number> prop2="myString" prop1={1} /> 
}

Alternatives

1.) A switch to class components would be easy, if you don't like the quirks.

class SampleSFCClass<P> extends React.Component<Sample2<P>> {
    render() {
        return <div>{this.props.prop1}</div>
    }
}

const MyApp2: React.SFC<{}> = (props) => {
    return <SampleSFCClass<number> prop2="myString" prop1={1} /> 
}

2.) You could even wrap the SFC in another function.

const withGeneric: <P>() => React.SFC<Sample2<P>> = <P extends {}>() => {
    return (props) => <div> {props.prop1}  {props.prop2} </div>;
}

const SampleSFCCalled: React.SFC<Sample2<string>> = withGeneric<string>();

const MyApp: React.SFC<{}> = (props) => {
        return <SampleSFCCalled prop2="myString" prop1="aString" />
}

It will work, but disadvantage might be slight performance decrease, because the SFC funtion is always recreated in each render cycle of the parent comp.

JSX/TSX compatibility issues with Generics:

In some constellations Typescript seems to have problems parsing generics in combination with JSX/TSX (up to most recent 3.0.1) due to ambiguous syntax. Compile error will then be:

"JSX element has no corresponding closing tag."

One of the contributors recommended to use the function syntax in this case (see issue).

When you stick to arrow function, workaround is to let the type parameter extend from object (shown here or here) to clarify its meant to be a generic lambda and not a JSX tag:

<P extends {}> or <P extends object>

Hope, that helps.

like image 177
ford04 Avatar answered Nov 15 '22 09:11

ford04