How to use generics in props in React in a functional component?
Asked Answered
R

8

97

In a class based component, I can easily write some code like this:

import * as React from 'react';
import { render } from 'react-dom';

interface IProps<T> {
    collapsed: boolean;
    listOfData: T[];
    displayData: (data: T, index: number) => React.ReactNode;
}

class CollapsableDataList<T> extends React.Component<IProps<T>> {
    render () {
        if (!this.props.collapsed) {
            return <span>total: {this.props.listOfData.length}</span>
        } else {
            return (
                <>
                    {
                        this.props.listOfData.map(this.props.displayData)
                    }
                </>
            )
        }
    }
}

render(
    <CollapsableDataList
        collapsed={false}
        listOfData={[{a: 1, b: 2}, {a: 3, b: 4}]}
        displayData={(data, index) => (<span key={index}>{data.a + data.b}</span>)}
    />,
    document.getElementById('root'),
)

Actually this CollapsableDataList component should be a functional component because it's stateless, but I can't figure out how to write a function component and use generics in props, any advise for me?

Reenareenforce answered 28/12, 2018 at 11:40 Comment(1)
See Also: Functional React Components with Generic Props in TypeScriptMagnetic
W
123

You can't create a functional component with a type annotation and make it generic. So this will NOT work as T is not defined and you can't define it on the variable level:

const CollapsableDataList : React.FunctionComponent<IProps<T>> = p => { /*...*/ } 

You can however skip the type annotation, and make the function generic and type props explicitly.

import * as React from 'react';
import { render } from 'react-dom';

interface IProps<T> {
    collapsed: boolean;
    listOfData: T[];
    displayData: (data: T, index: number) => React.ReactNode;
}
const CollapsableDataList = <T extends object>(props: IProps<T> & { children?: ReactNode }) => {
    if (!props.collapsed) {
        return <span>total: {props.listOfData.length}</span>
    } else {
        return (
            <>
                {
                    props.listOfData.map(props.displayData)
                }
            </>
        )
    }
}


render(
    <CollapsableDataList
        collapsed={false}
        listOfData={[{a: 1, b: 2}, {a: 3, c: 4}]}
        displayData={(data, index) => (<span key={index}>{data.a + (data.b || 0)}</span>)}
    />,
    document.getElementById('root'),
)
Whole answered 28/12, 2018 at 11:52 Comment(7)
I'm worrying about some HOC require you pass component which has explicit type (like ComponentClass or FunctionalComponent type), and then those functional components which don't have a type annotation won't pass the type check. (not test yet)Reenareenforce
@Reenareenforce Typescript uses structural typing, so the parameters of the function are more important than the declared type. You will still have problems with HOCs, because Typescript does not have higher order types. So the type parameter will get lost when you pass it to the HOC. But that is a general generic component issue, not so much a type annotation issue.Whole
I know it's not in the question either but it seems to me that when rendering CollapsableDataList, one should add a generic type like {a: number; b: number}, to better illustrate the benefit of being able to provide one. (Also, is there a particular reason for the optional children?)Right
@Right I just copied the children definition from react definitions I think. Adding the generic type parameter while possible is not necessary, T will be inferred. I always prefer to let the compiler do inference when it can.Whole
@TitianCernicova-Dragomir Right, I missed that the type can be inferred here, thanks! (I also like to use TS' inference capabilities as much as possible)Right
You can use PropsWithChildren<IProps<T>> and get rig of interface intersectionWombat
Thank you! Been searching for this for awhile, don't want to go back to class component unnecessarily.Rewire
B
33

The type React.FC is essentially this:

<P = {}>(props: PropsWithChildren<P>, context?: any) => ReactElement | null

so instead of this (which isn't allowed):

const Example: React.FC<Props<P>> = (props) => {
  // return a React element or null
}

you can use this:

const Example = <P extends unknown>(props: PropsWithChildren<Props<P>>): ReactElement | null => {
  // return a React element or null
}

For example:

const Example = <P extends unknown>({ value }: PropsWithChildren<{ value: P }>): ReactElement | null => {
  return <pre>{JSON.stringify(value)}</pre>
}

Or, more strictly, if the component doesn't use the children prop and won't return null:

const Example = <P>({ value }: { value: P }): ReactElement => {
  return <pre>{value}</pre>
}

then use the typed component as <Example<string> value="foo"/>

Bibliographer answered 2/7, 2020 at 21:16 Comment(0)
C
30
type Props<T> = {
    active: T;
    list: T[];
    onChange: (tab: T) => void;
};

export const Tabs = <T,>({ active, list, onChange }: Props<T>): JSX.Element => {
    return (
        <>
            {list.map((tab) => (
                <Button onClick={() => onChange(tab)} active={tab === active}>
                    {tab} 
                </Button>
            ))}
        </>
    );
};
Catalogue answered 4/2, 2021 at 19:31 Comment(5)
Notice the dangling , inside <T,> this fixes a bug with the compiler and allows using a simple generic.Vachill
Any more info on why the dangling , is needed and what bug it fixes?Gallium
TypeScript seems to think that <T> is the opening tag of a component named "T" in ".tsx" files that use JSX for React components. To move beyond this default interpretation of the transpiler to that of generics, the comma can be introduces as mentioned by @MatLipe as <T,>Imputation
When I am trying this, I am getting This JSX tag's 'children' prop expects a single child of type 'ReactNode', but multiple children were provided. near ` <Button onClick={() => onChange(tab)} active={tab === active}> {tab} </Button>`Cellaret
This did it for me. The other responses are way too over engineered.Froehlich
P
20

React 18, Typescript 4.6.3

interface IProps<T> {
  data: T[];
}
export const YourComponent = <T,>(props: IProps<T>) => {}

Usage:

const App = () => {
  const data : IDataProps[] = [
    {
      id:"1",
      label:"first banu"
    }
  ]
  return (
    <div className="App">
      <YourComponent data={data}/>
    </div>
  );
}
Parcae answered 13/9, 2022 at 17:8 Comment(2)
how to use this YourComponent?Emboss
What is the 2nd omitted type here <T,> ?Avaricious
I
14

Before addressing the functional component, I assume the original code example is missing the generic in JSX component as I don't see it passed to the IProps interface. I. e.:

interface Ab {
  a: number;
  b: number;
}

...

// note passing the type <Ab> which will eventually make it to your IProps<T> interface and cascade the type for listOfData
return (
<CollapsableDataList<Ab>
  collapsed={false}
  listOfData={[{a: 1, b: 2}, {a: 3, c: 4}]}
  ...
/>
)

Ok now with a little effort you actually can have a functional component with generic props.

You are stuck using 'modern' syntax though as it employs an assignment and arrow function which is of no use for your generic case:

// using this syntax there is no way to pass generic props
const CollapsableDataList: React.FC<IProps> = ({ collapsed, listOfData }) => {
  // logic etc.
  return (
  // JSX output
  );
}

Let's rewrite the variable assignment as a good old function:

// we are now able to to write our function component with generics
function CollapsableDataList<T>({ collapsed, listOfData }: IProps<T> & { children?: React.ReactNode }): React.ReactElement {
  // logic etc.
  return (
  // JSX output
  );
}

The children workaround is not necessarily needed if the component does not use the children prop but I've added it to highlight the fact that it has to be retyped manually as React.FC did that for us before.

Impasto answered 17/12, 2019 at 11:53 Comment(0)
C
2

supplement for #1.

If you want to export component as FunctionComponent and pass eslint displayName error.

you can do it bellow.

const yourComponentWithLowerCase: <T>(props: PropsWithChildren<Props<T>>) => ReactElement | null = (props) => {
  // code
}

export const YourComponentWithUpperCase = yourComponentWithLowerCase;
(YourComponentWithUpperCase as FunctionComponent).displayName = 'something'


Contraindicate answered 19/11, 2021 at 7:54 Comment(0)
R
1

This answer is a good example since it properly defines the props and the return type of the function.

As an alternative these can be defined as functions instead of arrow functions. This gets around the need to extend the prop type to hint to the TS compiler that this not a react component.

export function CollapsableDataList<T>(
  props: PropsWithChildren<IProps<T>>
): ReturnType<FunctionComponent<IProps<T>>> {
  // ...
}
Roland answered 22/11, 2021 at 14:20 Comment(0)
U
1

I have created a Simple Custom Button Component using React Native, showcasing a scenario of using generics with props in a React Functional Component.

import React from 'react';
import { Pressable, StyleSheet, Text } from 'react-native';

export type ButtonType = 'filled' | 'outline';

interface IButtonProps<T extends ButtonType> {
  type: T;
  onPress: () => void;
  filledText?: T extends 'filled' ? string : never;
  outlineText?: T extends 'outline' ? string : never;
}

const CustomButton = <T extends ButtonType>({
  type,
  filledText,
  outlineText,
  onPress,
}: IButtonProps<T>) => {
  return (
    <Pressable
      style={
        type === 'filled' ? styles.filledContainer : styles.outlineContainer
      }
      onPress={onPress}>
      <Text style={styles.textContainer}>{filledText || outlineText}</Text>
    </Pressable>
  );
};

export default CustomButton;

export const styles = StyleSheet.create({
  filledContainer: {
    flex: 1,
    padding: 16,
    backgroundColor: '#0096FF',
    borderRadius: 10,
  },
  outlineContainer: {
    flex: 1,
    padding: 16,
    borderWidth: 1,
    borderColor: '#0096FF',
    borderRadius: 10,
  },
  textContainer: {
    color: '#191970',
    textAlign: 'center',
    fontWeight: '500',
    fontSize: 22,
  },
});

For React.js users you will have to only change the JSX of the above code snippet accordingly.

Lets call the Custom Button Component by setting a type which will conditionally take in only the attributes related to a specific type or else a type error is shown.

<CustomButton
  type="filled"
  onPress={() => Alert.alert('Pressed')}
  filledText="Filled"
 />

enter image description here

 <CustomButton
   type="outline"
   onPress={() => Alert.alert('Pressed')}
   outlineText="Outline"
  />

enter image description here

Uncanonical answered 10/4, 2023 at 19:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.