DEV Community

Pedro Henrique Pires
Pedro Henrique Pires

Posted on • Edited on

React Generic Components

Some time ago, I came across the task of creating a table component for one of the applications I work on. Although it's a simple task, there are many opportunities when we talk about Developer Experience.

Let's start with something very simple, a component that takes two properties: data and columns.

function TableHead(props: Pick<Props, 'columns'>) {
  return <>...</>
}

function TableBody(props: Props) {
  return <>...</>
}

export function Table({ data, columns }: Props) {
  return (
    <table>
      <TableHead columns={columns} />
      <TableBody columns={columns} data={data} />
    </table>
  )
}
Enter fullscreen mode Exit fullscreen mode

I'll set aside the implementation of the component logic itself to make it easier to focus on what really matters for this post.

The data property here refers to the data that our table needs to render. To simplify things, we will consider a simple table that accepts string, number, or boolean as possible values, for example.

[{
  name: 'Pedro Henrique Pires',
  age: 27,
  github_url: 'http://github.com/pedrohenriquepires'
}]
Enter fullscreen mode Exit fullscreen mode

As for the columns property, it refers to the mapping of the columns in our table. We might want to have GitHub URL written in the header of our table instead of github_url.

For this purpose, we can define that our column object will have two properties: title and accessor, where title is the property that sets the column name, and accessor is the property that defines which property of the data object this column will render.

For typing this component, we can use something like this:

type TableData = Record<string, string | number | boolean>

type TableColumn = {
  title: string
  accessor: string
}

type Props = {
  data: TableData[]
  columns: TableColumn[]
}
Enter fullscreen mode Exit fullscreen mode

With this, we already have a properly typed component... do we?

Right away, it's possible to notice a problem that could potentially cause bugs in our component.

The accessor property of our column doesn't have any awareness of the properties that exist in our data, meaning it's very easy for someone to make a typing mistake and write gtihub_url instead of github_url, causing a failure in rendering that column.

Image showing that no error occurs when trying to use the property gtihub_url instead of github_url.

Not to mention that we don't have any autocomplete assistance when configuring the columns, making it a bit more challenging for those using the component.

Fortunately, there is a solution to these problems, and it's much simpler than it may seem.

Generic Types

With a few small modifications to our code, we can turn our component into a generic component and leverage TypeScript's inference to our advantage.

type TableData<T> = {
  [Property in keyof T]: string | number | boolean
};

type TableColumn<T> = {
  title: string
  accessor: keyof T
}

type Props<T> = {
  data: TableData<T>[]
  columns: TableColumn<T>[]
}
Enter fullscreen mode Exit fullscreen mode

We changed the type TableData to receive a generic type and used this type for typing the properties of the object.

We also changed the type TableColumn to receive a generic type, using the properties of this generic type (keyof) as possible values for our accessor property.

The type Props, which we use to define the properties of our component, was also changed to receive a generic type, and this type is passed to the TableData and TableColumn types.

To finish, we also need to modify our component:

function TableHead<T>(props: Pick<Props<T>, 'columns'>) {
  return <></>
}

function TableBody<T>(props: Props<T>) {
  return <></>
}

export function Table<T>({ data, columns }: Props<T>) {
  return (
    <table>
      <TableHead columns={columns} />
      <TableBody columns={columns} data={data} />
    </table>
  )
}
Enter fullscreen mode Exit fullscreen mode

The change here is also simple; we just added a generic parameter to the component and passed that parameter to the Props type.

Now, the accessor property has awareness of the properties available within data. We will start to have autocomplete to assist in using the component and errors when using properties that do not exist, preventing bugs in our application.

Image showing autocomplete with possible properties

Image showing an error when trying to use the property gtihub_url instead of github_url

Conclusion

With a few changes, we've managed to enhance the development experience and make our component more secure. If you're not yet familiar with generics, I strongly recommend checking out some materials on the topic. This video by Matt Pocock is an excellent guide to get started.

Disclaimer

With great power comes great responsibility. Often, we can end up adding unnecessary complexity to our code. This article demonstrates just one use case and should not be taken as a rule.

Top comments (0)