How to use generics in props in React in a functional component?

ReactjsTypescriptGenericsReact Props

Reactjs Problem Overview


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?

Reactjs Solutions


Solution 1 - Reactjs

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'),
)

Solution 2 - Reactjs

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"/>

Solution 3 - Reactjs

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>
            ))}
        </>
    );
};

Solution 4 - Reactjs

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.

Solution 5 - Reactjs

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'


Solution 6 - Reactjs

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>>> {
  // ...
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionhronroView Question on Stackoverflow
Solution 1 - ReactjsTitian Cernicova-DragomirView Answer on Stackoverflow
Solution 2 - ReactjsAlf EatonView Answer on Stackoverflow
Solution 3 - Reactjsosorina irinaView Answer on Stackoverflow
Solution 4 - ReactjsdmudroView Answer on Stackoverflow
Solution 5 - ReactjstaoliujunView Answer on Stackoverflow
Solution 6 - ReactjsDerekView Answer on Stackoverflow