React的通用道具指南

80 阅读3分钟

上一篇文章中,我们探讨了React组件道具可以用TypeScript进行强类型化的不同方式。在这篇文章中,我们将介绍一种更高级的方法,即为道具使用一个通用类型

使用案例

当你想创建一个可重复使用的数据驱动的组件时,通用道具很有用,因为数据的形状是不同的。可重用的表单、列表和表格组件就是通用道具很方便的例子。在这篇文章中,我们将创建一个简单的可重复使用的表格组件。

语法

通用道具类型的语法如下:

type Props<T> = { ... };

T 是消费者可以传入的数据类型的一个参数。当我们实现我们的例子时,我们将使用一个更有意义的名字而不是T

如果我们想用一个接口来表示该类型,那么语法将如下:

interface Props<T> { ... }

更棘手的是将这个类型应用于React组件:

const ReusableComponent = <T extends object>(props: Props<T>) => { ... }

上面的语法是针对一个箭头函数组件的。下面是普通函数组件声明的语法:

function ReusableComponent<T extends object>(props: Props<T>) { ... }

我们在函数参数的括号前定义消费者可以传入组件的类型。注意,我们使用了T extends object ,而不仅仅是T ,否则会出现解析错误(我们也可以使用<T,> 来避免解析错误)。

如果我们想让数据有一个特定的属性,那么我们可以指定这个属性来代替object 。下面的例子要求数据有一个name 属性:

<T extends {name: string}>

现在我们开始了解语法了,让我们来看看一个用例。

通用的表格组件props

我们将实现一个通用的表格组件。这里是我们的通用道具类型:

type Props<DataItem> = {
  data: DataItem[];
  onRowClick: (item: DataItem) => void;
};

我们选择将类型参数命名为DataItem ,这是数据中每个项目的类型。道具包含一个data 道具,它是一个数据项的数组。道具还包含一个事件道具,onRowClick 。当表格中的某一行被点击时,该事件将被调用,并将该行的数据项传递给它。

下面是表格组件实现的开始:

const Table = <T extends object>({ data, onRowClick }: Props<T>) => {}

我们对dataonRowClick 道具进行了重构,这样我们就可以在实现中直接引用它们。

我们的表组件可以被消费,如下所示:

<Table
  data={[
    { name: "Bill", score: 15 },
    { name: "Jane", score: 18 },
    { name: "Fred", score: 10 }
  ]}
  onRowClick={row => console.log(row)}
/>

请注意,我们不需要向组件传递数据类型,因为TypeScript会巧妙地推断它。

渲染表格的列标题

我们将渲染表格中的列标题。在这之前,我们将检查是否有任何数据,如果没有,我们将不渲染:

const Table = <DataItem extends object>({ data, onRowClick }: Props<DataItem>) => {
  if (data.length === 0) {    return null;  }};

现在我们来渲染列标题:

const Table = <DataItem extends object>({ data, onRowClick }: Props<DataItem>) => {
  if (data.length === 0) {
    return null;
  }
  return (    <table>      <thead>        <tr>          {Object.keys(data[0]).map(key => (            <th key={key}>{key}</th>          ))}        </tr>      </thead>    </table>  );};

我们使用Object.keys 来获取数据中的所有属性,然后用map 方法来迭代它们,输出th 元素。

渲染表格数据

让我们继续渲染表中的数据:

const Table = <T extends object>({ data, onRowClick }: Props<T>) => {
  ...
  return (
    <table>
      <thead>
        ...
      </thead>
      <tbody>        {data.map((item: DataItem, index: number) => (          <tr key={index}>            {(Object.keys(data[0]) as Array<keyof DataItem>).map(key => (              <td key={key.toString()}>{item[key]}</td>            ))}          </tr>        ))}      </tbody>    </table>
  );
};

请注意,我们必须在Object.keys 的结果上使用一个类型断言,因为默认情况下,它将返回string[] ,这意味着item[key] 会引发一个类型错误。

实现行点击

最后一步是实现行的点击:

<tbody>
    {data.map((item: DataItem, index: number) => (
        <tr key={index} onClick={() => onRowClick(item)}>        {(Object.keys(data[0]) as Array<keyof DataItem>).map(key => (
            <td key={key.toString()}>{item[key]}</td>
        ))}
        </tr>
    ))}
</tbody>

让我们检查一下onRowClick 参数的类型在我们的消费者中被推断为什么。

onRowClick type

很好!

这显示了通用道具的威力,因为即使传入的数据类型可能不同,道具对消费者来说仍然是强类型的。

这里有一个CodeSandbox中这个例子的链接

总结

通用道具允许我们实现通用组件,在不影响类型安全的情况下处理不同形状的数据。这个语法在一开始可能有点奇怪,但过了一段时间后就很有意义了,而且其结果是非常值得的。