Как использовать дженерики в реквизитах в React в функциональном компоненте?

В компоненте на основе классов я могу легко написать такой код:

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

На самом деле этот CollapsableDataList компонент должен быть функциональным компонентом, потому что он не имеет состояния, но я не могу понять, как написать функциональный компонент и использовать дженерики в реквизитах, какие-нибудь советы для меня?


person hronro    schedule 28.12.2018    source источник


Ответы (3)


Вы не можете создать функциональный компонент с аннотацией типа и сделать его универсальным. Таким образом, это НЕ будет работать, поскольку T не определен, и вы не можете определить его на уровне переменных:

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

Однако вы можете пропустить аннотацию типа и сделать функцию универсальной и явно ввести props.

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'),
)
person Titian Cernicova-Dragomir    schedule 28.12.2018
comment
Меня беспокоит, что некоторые HOC требуют, чтобы вы передавали компонент с явным типом (например, ComponentClass или FunctionalComponent тип), а затем те функциональные компоненты, которые не имеют аннотации типа, не пройдут проверку типа. (еще не тестировал) - person hronro; 28.12.2018
comment
@hronro Typescript использует структурную типизацию, поэтому параметры функции более важны, чем объявленный тип. У вас по-прежнему будут проблемы с HOC, потому что Typescript не имеет типов более высокого порядка. Таким образом, параметр типа будет потерян, когда вы передадите его HOC. Но это общая проблема общих компонентов, а не проблема аннотации типов. - person Titian Cernicova-Dragomir; 28.12.2018
comment
Я знаю, что это тоже не вопрос, но мне кажется, что при рендеринге CollapsableDataList нужно добавить общий тип, такой как {a: number; b: number}, чтобы лучше проиллюстрировать преимущества возможности его предоставления. (Кроме того, есть ли особая причина для необязательных детей?) - person AndyO; 18.05.2019
comment
@AndyO Я только что скопировал определение children из определений реакции, я думаю. Добавление параметра универсального типа, пока возможно, не требуется, будет выведено T. Я всегда предпочитаю позволять компилятору делать выводы, когда это возможно. - person Titian Cernicova-Dragomir; 19.05.2019
comment
@ TitianCernicova-Dragomir Правильно, я упустил из виду, что здесь можно вывести тип, спасибо! (Мне также нравится максимально использовать возможности вывода TS) - person AndyO; 19.05.2019
comment
Вы можете использовать PropsWithChildren ‹IProps ‹T›› и получить настройку пересечения интерфейсов - person brianconnoly; 29.10.2020

Тип React.FC по сути таков:

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

поэтому вместо этого (что запрещено):

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

вы можете использовать это:

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

Например:

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

Или, более строго, если компонент не использует свойство children и не возвращает null:

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

затем используйте типизированный компонент как <Example<string> value="foo"/>

person Alf Eaton    schedule 02.07.2020

Прежде чем обратиться к функциональному компоненту, я предполагаю, что в исходном примере кода отсутствует универсальный компонент в JSX-компоненте, поскольку я не вижу, чтобы он передавался в интерфейс IProps. 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}]}
  ...
/>
)

Хорошо, теперь, приложив немного усилий, вы действительно можете получить функциональный компонент с общими реквизитами.

Вы застряли в использовании `` современного '' синтаксиса, поскольку он использует функцию присваивания и стрелки, которая бесполезна для вашего общего случая:

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

Давайте перепишем присвоение переменной как старый добрый 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
  );
}

Обходной путь children не обязательно нужен, если компонент не использует свойство children, но я добавил его, чтобы подчеркнуть тот факт, что его нужно повторно набирать вручную, как React.FC сделал это для нас раньше.

person dmudro    schedule 17.12.2019