如何在功能组件中的 React props 中使用泛型?
How to use generics in props in React in a functional component?
在基于 class 的组件中,我可以轻松编写如下代码:
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
组件应该是一个功能组件,因为它是无状态的,但我不知道如何编写一个功能组件并在 props 中使用泛型,有什么建议吗?
您不能创建带有类型注释的功能组件并使其通用。所以这不会起作用,因为 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'),
)
在处理功能组件之前,我假设原始代码示例缺少 JSX 组件中的泛型,因为我没有看到它传递给 IProps
接口。即:
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}]}
...
/>
)
好的,现在稍加努力,您实际上可以拥有一个带有通用道具的功能组件。
您无法使用 'modern' 语法,因为它使用了对您的一般情况没有用的赋值和箭头函数:
// 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
这样做了对于我们之前。
类型 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
属性并且不会 return null
:
const Example = <P>({ value }: { value: P }): ReactElement => {
return <pre>{value}</pre>
}
然后使用键入的组件作为 <Example<string> value="foo"/>
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>
))}
</>
);
};
#1 的补充。
如果你想将组件导出为FunctionComponent并通过eslint displayName报错。
您可以按照下面的方式进行。
const yourComponentWithLowerCase: <T>(props: PropsWithChildren<Props<T>>) => ReactElement | null = (props) => {
// code
}
export const YourComponentWithUpperCase = yourComponentWithLowerCase;
(YourComponentWithUpperCase as FunctionComponent).displayName = 'something'
是一个很好的例子,因为它正确地定义了 props 和函数的 return 类型。
作为替代方案,这些可以定义为函数而不是箭头函数。这解决了扩展 prop 类型以提示 TS 编译器这不是 React 组件的需要。
export function CollapsableDataList<T>(
props: PropsWithChildren<IProps<T>>
): ReturnType<FunctionComponent<IProps<T>>> {
// ...
}
在基于 class 的组件中,我可以轻松编写如下代码:
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
组件应该是一个功能组件,因为它是无状态的,但我不知道如何编写一个功能组件并在 props 中使用泛型,有什么建议吗?
您不能创建带有类型注释的功能组件并使其通用。所以这不会起作用,因为 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'),
)
在处理功能组件之前,我假设原始代码示例缺少 JSX 组件中的泛型,因为我没有看到它传递给 IProps
接口。即:
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}]}
...
/>
)
好的,现在稍加努力,您实际上可以拥有一个带有通用道具的功能组件。
您无法使用 'modern' 语法,因为它使用了对您的一般情况没有用的赋值和箭头函数:
// 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
这样做了对于我们之前。
类型 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
属性并且不会 return null
:
const Example = <P>({ value }: { value: P }): ReactElement => {
return <pre>{value}</pre>
}
然后使用键入的组件作为 <Example<string> value="foo"/>
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>
))}
</>
);
};
#1 的补充。
如果你想将组件导出为FunctionComponent并通过eslint displayName报错。
您可以按照下面的方式进行。
const yourComponentWithLowerCase: <T>(props: PropsWithChildren<Props<T>>) => ReactElement | null = (props) => {
// code
}
export const YourComponentWithUpperCase = yourComponentWithLowerCase;
(YourComponentWithUpperCase as FunctionComponent).displayName = 'something'
作为替代方案,这些可以定义为函数而不是箭头函数。这解决了扩展 prop 类型以提示 TS 编译器这不是 React 组件的需要。
export function CollapsableDataList<T>(
props: PropsWithChildren<IProps<T>>
): ReturnType<FunctionComponent<IProps<T>>> {
// ...
}