Functional React components with generic props in TypeScript

栏目: IT技术 · 发布时间: 5年前

内容简介:One of the qualities of our code that we should aim for is reusability. Also, following theIf you would like to revisit the basics of generics first, check outOne of the things that contribute to good design is

One of the qualities of our code that we should aim for is reusability. Also, following the D on’t R epeat Y ourself principle makes our code more elegant. In this article, we explore writing functional React components with TypeScript using generic props. By doing so, we can create reusable and flexible components.

If you would like to revisit the basics of generics first, check out TypeScript Generics. Discussing naming conventions

Defining the use case

One of the things that contribute to good design is consistency . Implementing it means having lots of similar parts of the application. One of the components that we encounter is a table. Let’s create an example of such. For starters, we make it in a way that it displays a certain entity – posts.

interface Post {
  id: number;
  title: string;
  body: string;
}
import React, { FunctionComponent } from 'react';
import Post from '../../interfaces/Post';
import Head from './Head';
import Row from './Row';
 
interface Props {
  posts: Post[];
}
 
const PostsTable: FunctionComponent<Props> = ({
  posts,
}) => {
  return (
    <table>
      <Head/>
      <tbody>
        {
          posts.map(post => (
            <Row
              key={post.id}
              title={post.title}
              body={post.body}
            />
          ))
        }
      </tbody>
    </table>
  );
};

The above code is pretty straightforward. Our PostsTable takes an array of posts and displays all of them. Let’s fetch the posts and provide our component with them.

import React from 'react';
import usePostsLoading from './usePostsLoading';
import PostsTable from './PostsTable';
 
const PostsPage = () => {
  const { posts, isLoading } = usePostsLoading();
  return (
    <div>
      {
        !isLoading && (
          <PostsTable
            posts={posts}
          />
        )
      }
    </div>
  );
};

If  you want to know how to write hooks like the one above, check out Race conditions in React and beyond. A race condition guard with TypeScript

The header always displays a predefined set of properties.

const Head = () => {
  return (
    <thead>
      <tr>
        <th>Title</th>
        <th>Body</th>
      </tr>
    </thead>
  );
};

The same thing with the row – it displays just the title and the body.

interface Props {
  title: string;
  body: string;
}
 
const Row: FunctionComponent<Props> = ({
  title,
  body,
}) => {
  return (
    <tr>
      <td>{title}</td>
      <td>{body}</td>
    </tr>
  );
};

While the above PostsTable component is reusable to some extent, it is not very flexible. We can just use it to display posts.

The fact is that there are high chances that we need more tables in our application, not only for posts. Creating a separate one for every type of entity is a solution, but not the most refined one.

Introducing generic components

First, let’s define the requirements for our generic table component. It needs to:

  • accept an array of entities of any type,
  • process an array of properties that we want to display in a table.

Taking all of the above into consideration we can specify our Props interface.

interface Props<ObjectType> {
  objects: ObjectType[];
  properties: {
    key: keyof ObjectType,
    label: string,
  }[];
}

First, we have an array of objects . Second, we have an array of  properties . Our property consists of a  key and a  title .

A crucial thing is that the key needs to match the properties of the object. To do so, we use the  keyof keyword to create a union of string literal types . If you want to learn more, check out  More advanced types with TypeScript generics

To better grasp the concept, this is an example of props that implement the above interface:

{
  objects: posts,
  properties: [
    {
      key: 'title',
      label: 'Title'
    },
    {
      key: 'body',
      label: 'Content'
    }
  ]
}

Defining a generic component

In all of the above examples, we’ve used the generic FunctionComponent provided by React. Unfortunately, it would be rather troublesome to use it with generic interfaces. What we can do is to type the props directly.

If we look into the FunctionComponent interface, it uses  PropsWithChildren , and we can also make use of that.

To type our component, we can choose one of the two approaches. First would be to use an arrow function:

const Table = <ObjectType, >(
  props: PropsWithChildren<Props<ObjectType>>
) => {

The trailing comma in < ObjectType , > is added due to contraints of the  . tsx file extension. You can read more in the  TypeScript Generics. Discussing naming conventions

The second approach is to use a more classic function:

function Table<ObjectType>(
  props: PropsWithChildren<Props<ObjectType>>
) {

The latter looks a bit cleaner, in my opinion. Therefore, I will use it in all of the below examples.

In our Table component, we want to iterate through all of the objects. To display a row for every object, we need to pass the  key prop. The most suitable property would be  id . To make sure that our objects have it, we can add a  constraint .

function Table<ObjectType extends { id: number }>(
  props: PropsWithChildren<Props<ObjectType>>
) {

Now we can be sure that the ObjectType contains the id.

Implementing a fully working table

Our Table needs a row. It is also generic and accepts a single object and an array of properties.

interface Props<ObjectType> {
  object: ObjectType;
 
}
function Row<ObjectType extends { id: number }>(
  { object, properties }: Props<ObjectType>
) {
  return (
    <tr>
      {
        properties.map(property => (
          <td key={String(property.key)}>
            {object[property.key]}
          </td>
        ))
      }
    </tr>
  );
}

The Head , on the other hand, does not need to be generic. We provide it just with a list of properties.

interface Props {
  properties: {
    key: number | symbol | string,
    title: string,
  }[];
}
const Head: FunctionComponent<Props> = ({
  properties,
}) => {
  return (
    <thead>
      <tr>
        {
          properties.map(property => (
            <th key={String(property.key)}>{property.title}</th>
          ))
        }
      </tr>
    </thead>
  );
};

A thing worth pointing out is that the key above is of type  number | symbol | string . The above is because the key of an object by default is of the type  number | symbol | string .

To change the above behavior, we could force the keys to be of a string type by using keyof ObjectType & string .

properties: {
  key: keyof ObjectType,
  title: string,
}[];

Alternatively, we could also use the keyofStringsOnly option when compiling.

Putting it all together

Once we’ve defined all the pieces, we can put them together.

interface Props<ObjectType> {
  objects: ObjectType[];
  properties: {
    key: keyof ObjectType,
    title: string,
  }[];
}
function Table<ObjectType extends { id: number }>(
  { objects, properties }: PropsWithChildren<Props<ObjectType>>,
) {
  return (
    <table>
      <Head
        properties={properties}
      />
      <tbody>
        {
          objects.map(object => (
            <Row
              key={object.id}
              object={object}
              properties={properties}
            />
          ))
        }
      </tbody>
    </table>
  );
}

Even though the implementation of the above component might seem rather complex, its usage is very straightforward.

import React from 'react';
import usePostsLoading from './usePostsLoading';
import Table from '../Table';
 
const PostsPage = () => {
  const { posts, isLoading } = usePostsLoading();
  return (
    <div>
      {
        !isLoading && (
          <Table
            objects={posts}
            properties={[
              {
                key: 'title',
                title: 'Title'
              },
              {
                key: 'body',
                title: 'Body'
              },
            ]}
          />
        )
      }
    </div>
  );
};

The TypeScript compiler understands that the ObjectType above is a post. Due to that, we can only provide properties that can be found in the Post interface.

On the other hand, we could make the ObjectType explicit by providing the interface.

<Table<Post>
  objects={posts}
  properties={[
    {
      key: 'title',
      title: 'Title'
    },
    {
      key: 'body',
      title: 'Body'
    },
  ]}
/>

Summary

Thanks to all of the above work, we now have a reusable Table component that we can use to display any entity. By doing so, we aim for reusability and make following the DRY principle easier. You can go ahead and implement more features in our  Table . Thanks to it being generic, we can have them all across our application.


以上所述就是小编给大家介绍的《Functional React components with generic props in TypeScript》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

计算理论导引

计算理论导引

[美]Michael Sipser / 张立昂、王捍贫、黄雄 / 机械工业出版社 / 2000-2 / 30.00元

本书由计算理论领域的知名权威Michael Sipser撰写。他以独特的视角,综合地描述了计算机科学理论,并以清新的笔触、生动的语言给出了宽泛的数学理论,而并非拘泥于某些低层次的技术细节。在证明之前,均有“证明思路”,帮助读者理解数学形式下蕴涵的概念。同样,对于算法描述,均以直观的文字,而非伪代码给出,从而将注意力集中于算法本身,而不是某些模型。本书的内容包括三个部分:自动机与语言、可计算性理论和一起来看看 《计算理论导引》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

在线进制转换器
在线进制转换器

各进制数互转换器

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具