96 votes

Comment utiliser les génériques dans les props en React dans un composant fonctionnel ?

Dans un composant basé sur des classes, je peux facilement écrire un code comme celui-ci :

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

En fait, cette CollapsableDataList devrait être un composant fonctionnel parce qu'il est sans état, mais je n'arrive pas à trouver comment écrire un composant fonctionnel et utiliser des génériques dans les props, un conseil pour moi ?

121voto

devilmaster Points 439

Vous ne pouvez pas créer un composant fonctionnel avec une annotation de type et le rendre générique. Cela ne fonctionnera donc PAS comme T n'est pas défini et vous ne pouvez pas le définir au niveau de la variable :

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

Vous pouvez cependant sauter l'annotation de type, et rendre la fonction générique et de type props explicitement.

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

0 votes

Je suis préoccupé par le fait que certains HOC exigent que vous passiez un composant qui a un type explicite (comme ComponentClass o FunctionalComponent ), et les composants fonctionnels qui n'ont pas d'annotation de type ne passeront pas le contrôle de type. (pas encore testé)

0 votes

@hronro Typescript utilise le typage structurel, donc les paramètres de la fonction sont plus importants que le type déclaré. Vous aurez toujours des problèmes avec les HOC, car Typescript n'a pas de types d'ordre supérieur. Le paramètre de type sera donc perdu lorsque vous le passerez au HOC. Mais il s'agit là d'un problème général de composant générique, et non d'un problème d'annotation de type.

0 votes

Je sais que ce n'est pas non plus dans la question, mais il me semble que lorsque le rendu CollapsableDataList il faut ajouter un type générique comme {a: number; b: number} pour mieux illustrer l'avantage de pouvoir en fournir un. (Par ailleurs, y a-t-il une raison particulière pour les enfants facultatifs ?)

32voto

Alf Eaton Points 1209

Le type React.FC est essentiellement ceci :

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

donc à la place de ceci (qui n'est pas autorisé) :

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

vous pouvez utiliser ceci :

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

Par exemple :

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

Ou, plus strictement, si le composant n'utilise pas l'option children prop et ne reviendra pas null :

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

puis utiliser le composant typé comme <Example<string> value="foo"/>

30voto

osorina irina Points 31
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>
            ))}
        </>
    );
};

8 votes

Remarquez le , suspendu à l'intérieur de <T,> cela corrige un bug du compilateur et permet d'utiliser un simple générique.

14voto

dmudro Points 179

Avant d'aborder le composant fonctionnel, je suppose que l'exemple de code original ne contient pas le composant générique en JSX, car je ne vois pas qu'il soit passé à la fonction IProps l'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, maintenant, avec un petit effort, vous pouvez réellement avoir un composant fonctionnel avec des accessoires génériques.

Vous êtes cependant coincé par l'utilisation de la syntaxe "moderne", car elle utilise une fonction d'affectation et de flèche qui n'est d'aucune utilité dans votre cas générique :

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

Réécrivons l'affectation de variable comme un bon vieux 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
  );
}

El children n'est pas nécessairement nécessaire si le composant n'utilise pas la prop enfant mais je l'ai ajouté pour souligner le fait qu'il doit être retapé manuellement en tant que React.FC a fait ça pour nous avant.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X