How to build a custom React table component with Typescript (Part 1)
Tags: webdev, react, nextjs, javascript
Published: 2024-06-16 20:29:44

## Introduction

The purpose of this article is to show you in a simple way, how to create a fully-typed, reusable Table component. For this, we will be using the Material UI (MUI) Library React table component: https://mui.com/material-ui/react-table to move fast without writing any styling but this could be applied to any table component library

A few things to help you understand how the MUI Library Table component works:

1. Table Head - renders a <thead></thead> that holds the headings for each column of the table
2. Table Body - renders a <tbody></tbody> that holds the rows that render the data for the table
3. Pagination (optional) - library-provided pagination component - we won't cover it because it's not our focus in this article

Let's look at the basic components that make each of the parts of a table as mentioned above:

#### Table Head

This is a component that renders the `<th></th>` element for each column and wraps it with the right html tags to construct a table header. Each column of the table has a heading `<th></th>`. So let's define the shape of each column so we can build it out:

```ts
interface Column {
  id: string;
  label: string;
}
```

Let's model the Column type and create the columns array that we will use to create the Table Header:

```ts
const columns: Column[] = [
  { id: "name", label: "Name" },
  { id: "email", label: "Email address" },
  {
    id: "phoneNumber",
    label: "Phone Number",
  },
  {
    id: "age",
    label: "Age",
  },
];
```

Now, let's create the table header:

```tsx
<TableHead>
  <TableRow>
    {columns.map((column) => (
      <TableCell key={column.id}>{column.label}</TableCell>
    ))}
  </TableRow>
</TableHead>
```

And this will take the labels of each column provided to render this:

```html
<thead>
  <tr>
    <th>Name</th>
    <th>Email address</th>
    <th>Phone Number</th>
    <th>Age</th>
  </tr>
</thead>
```

#### Table Body

The body of our table is really not different from the html table body. We will import the `TableBody` component from MUI - this renders the `<tbody></tbody>` element. And then we just provide the rows of the data that it would need to construct the `<td></td>` for each row of the table.

To achieve this, we will have an interface that mirrors the structure of the data we want to render. In our case, we will model a Table of `Person` data:

```ts
interface Person {
  name: string;
  email: string;
  age: number;
  phoneNumber: string;
}
```

And let's create some sample data to use with it:

```ts
const rows: Person[] = [
  {
    name: "John Doe",
    email: "john.doe@example.com",
    age: 25,
    phoneNumber: "+1234567890",
  },
  {
    name: "Jane Smith",
    email: "jane.smith@example.com",
    age: 30,
    phoneNumber: "+1987654321",
  },
  {
    name: "Alice Johnson",
    email: "alice.johnson@example.com",
    age: 35,
    phoneNumber: "+1122334455",
  },
];
```

Now, let's create the table body:

```tsx
<TableBody>
  {rows.map((row) => (
    <TableRow key={row.name}>
      {columns.map((column) => (
        <TableCell key={column.id}>{row[column.id]}</TableCell>
      ))}
    </TableRow>
  ))}
</TableBody>
```

And this will take each object in the array we provided to render this:

```html
<tbody>
  <tr>
    <td>John Doe</td>
    <td>john.doe@example.com</td>
    <td>25</td>
    <td>+1234567890</td>
  </tr>
  ...more rows as is provided in the array
</tbody>
```

And now, we can combine it all to render the whole table.

```tsx
<TableContainer>
  <Table>
    <TableHead>
      <TableRow>
        {columns.map((column) => (
          <TableCell key={column.id}>{column.label}</TableCell>
        ))}
      </TableRow>
    </TableHead>
    <TableBody>
      {rows.map((row) => (
        <TableRow key={row.name}>
          {columns.map((column) => (
            <TableCell key={column.id}>{row[column.id]}</TableCell>
          ))}
        </TableRow>
      ))}
    </TableBody>
  </Table>
</TableContainer>
```

You can play around with the code here {% embed https://stackblitz.com/edit/react-hegdt2?embed=1&file=People.tsx %} or click [here](https://react-hegdt2.stackblitz.io) to view how it looks in the browser

P.S. _You can click through the links on the page to see all the tables. If you check the contents of each component, you will notice similarities between them. Actually, the major difference is the structure of the data and the columns of the table. This shows an opportunity to refactor our code to reduce duplication by creating a reusable table component_

But before we proceed to do this, if you look at this part of the `TableBody` inside `Person.tsx` [here](https://stackblitz.com/edit/react-hegdt2?embed=1&file=Person.tsx), you will notice there is a typescript warning there.

![TS error - wrong array index](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fk9m8oerd2mc1m99veac.png)

This is because to access an object using the index notation in typescript, you have to use an index that matches an existing property of the object. In our case, we defined `column.id` as a string and that's why we are getting this error: `No index signature with a parameter of type 'string' was found on type 'Data'.`.

Since the `id` property of each column we have matches each property of the objects we provided in the array of rows, we can easily change the type of our `Column.id` to match this.

```ts
interface Column {
  id: keyof Person;
  label: string;
}
```

And now with this simple change, for each column object i.e. `{ id: 'name', label: 'Name' }`, we can use the `id` it to access each `Person` object in the array of `rows`.

Here is what our final code looks like: {% embed https://stackblitz.com/edit/react-hegdt2-xnzjad?embed=1&file=People.tsx %}

And with the wave of our TS wand, we can see that the error has vanished into thin air 😀

![TS Error fixed - object index](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/swdweyn51e7dtmalkr0x.png)

And with that fix, we are ready to proceed to create a component we can resuse to render the `People` and `Countries` pages respectively and re-use as many things as we can.

### Create a Table component

Before we discuss how I think we should start approaching this, I would like to point out the problem first and then we can look at a quick solution we can consider:
If you take a look at our `Countries.tsx` and `People.tsx` files, you will notice that the visible difference is just table header labels and the structure of the objects in the `rows` array.
This means we can abstract away the repetitve lines of code and reuse them from inside a component.

As discussed earlier, since our table is made up of two parts (`Header` and `Body`), we will start by breaking this component into these two parts.

To make our `CustomTable` component configurable from outside it, we have to make provision to allow this using `Props`. If you find `Props` strange, You can visit the [React Documentation](https://react.dev/learn/passing-props-to-a-component) to read up on this and then come back to this article.

Since this is a Typescript project, here is what I like to do first, define the interface of our `Props`

The interface for a `Person` is this:

```ts
interface Column {
  id: keyof Person;
  label: string;
}
```

And the one for a `Country` is this:

```ts
interface Column {
  id: keyof Country;
  label: string;
}
```

A quick look at the above code will reveal that the only difference is the type of the `id` inside each column. We need to find a way to create a Column that would be `generic` enough to handle any type. And that's where [Typescript Generics](https://www.typescriptlang.org/docs/handbook/2/generics.html) will come handy as it allows us to create a dynamic types that can be re-usable without being tied to a particular type:
Here is what I mean by that:

```ts
interface Column<T> {
  id: keyof T;
  label: string;
}
```

We just created a `Column` interface that expects anyone using it to provide the type to it and it can use it to add type safety wherever it is used.

And for our rows, since it's just an array of objects, it's pretty straightforward too.
it would just be this:

```ts
type Row = T[];
```

And now, we can combine the two to form the shape of the prop to pass to the `CustomTable`

```ts
interface Props<T> {
  columns: Column<T>[];
  rows: T[];
}
```

Since we have got that out of the way, let's use this to create a reusable `CustomTable` component:

- Let's start by creating a file: `table.tsx`.
  _P.S You can give yours any name of your choice_

- Next, we will add the data types we declared above at the top of the file.

```ts
export interface Column<T> {
  id: keyof T;
  label: string;
}

interface Props<T> {
  columns: Column<T>[];
  rows: T[];
}
```

_NOTE: we added the `export` statement to the `Column` interface because we need to use it inside the `Countries.tsx` and `People.tsx` files to provide type inference for the rows and columns arrays_

_Also, there is no point creating a dedicated type for the `rows` property since it's just an array of the type provided i.e. `T[]` and that's why I used it directly._

- Next, we want to create the function that would be exported as the component. We will also use the props provided to the component to access the rows and columns.

```tsx
export default function CustomTable<T>(props: Props<T>) {
  const { columns, rows } = props;

  return (
    <Paper>
      <TableContainer>
        <Table>
          <TableHead>
            <TableRow>
              {columns.map((column, index) => (
                <TableCell key={index}>{column.label}</TableCell>
              ))}
            </TableRow>
          </TableHead>
          <TableBody>
            {rows.map((row, index) => {
              return (
                <TableRow key={index}>
                  {columns.map((column, index) => {
                    return <TableCell key={index}>{row[column.id]}</TableCell>;
                  })}
                </TableRow>
              );
            })}
          </TableBody>
        </Table>
      </TableContainer>
    </Paper>
  );
}
```

_NOTE: I used the index as the keys of the array I mapped through here because I am sure the items are not going to change during each render. If you want to know why using array indexes as keys could be a bad idea, you can read about it [here](https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key)_

Now, we can use the component we created to re-build our two tables `Countries.tsx` and `People.tsx`.

### People.tsx

```tsx
import CustomTable, { Column } from "./custom-table";

interface Person {
  name: string;
  email: string;
  age: number;
  phoneNumber: string;
}

const columns: Column<Person>[] = [
  { id: "name", label: "Name" },
  { id: "email", label: "Email address" },
  {
    id: "phoneNumber",
    label: "Phone Number",
  },
  {
    id: "age",
    label: "Age",
  },
];

const rows: Person[] = [
  {
    name: "John Doe",
    email: "john.doe@example.com",
    age: 25,
    phoneNumber: "+1234567890",
  },
  {
    name: "Jane Smith",
    email: "jane.smith@example.com",
    age: 30,
    phoneNumber: "+1987654321",
  },
  {
    name: "Alice Johnson",
    email: "alice.johnson@example.com",
    age: 35,
    phoneNumber: "+1122334455",
  },
];

export default function People() {
  return <CustomTable columns={columns} rows={rows} />;
}
```

### Countries.tsx

```tsx
import CustomTable, { Column } from "./custom-table";

interface Country {
  name: string;
  code: string;
  population: number;
  size: number;
  density: number;
}

const columns: Column<Country>[] = [
  { id: "name", label: "Name" },
  { id: "code", label: "ISO\u00a0Code" },
  {
    id: "population",
    label: "Population",
  },
  {
    id: "size",
    label: "Size\u00a0(km\u00b2)",
  },
  {
    id: "density",
    label: "Density",
  },
];
const rows: Country[] = [
  {
    name: "India",
    code: "IN",
    population: 1324171354,
    size: 3287263,
    density: 1324171354 / 3287263, // ideally, should be computed dynamically by dividing each population by size,
  },
  {
    name: "China",
    code: "CN",
    population: 1403500365,
    size: 9596961,
    density: 1403500365 / 9596961,
  },
  {
    name: "Italy",
    code: "IT",
    population: 60483973,
    size: 301340,
    density: 60483973 / 301340,
  },
];

export default function Countries() {
  return <CustomTable columns={columns} rows={rows} />;
}
```

Here is the final code if you want to check it out:
{% embed https://stackblitz.com/edit/react-hegdt2-c52far?embed=1&file=People.tsx %}

Now we can render a million different tables, all we have to do is provide the rows and the column configuration.

### Extending the table functionality

Now, what we have created so far works well for almost every table if it's just to render some data which is what most tables do. But imagine if you had to render your table rows in somewhat similar fashion to this table.

![Table with a custom column](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w69txchdo6dnf4tlb7hf.png)

To be able to support this behavior, we have to extend the possibilities of our `CustomTable` component but I won't cover that right now.

## Conclusion

In this article, we've taken a detailed look at how to create a fully-typed, reusable Table component leveraging the styling the Material UI (MUI) Library in React provides. We started by understanding the basic structure of a table, focusing on the Table Head and Table Body components. We explored how to define and use TypeScript interfaces to model our data, ensuring type safety and reducing errors.

We walked through the creation of a simple table, then moved on to build a more complex, reusable `CustomTable` component. This component uses TypeScript generics to handle different data types, making it flexible and versatile for various use cases. By creating a `CustomTable`, we streamlined our code, eliminating redundancy and making it easier to manage and extend.

We also discussed how to handle TypeScript warnings and errors, ensuring our code remains clean and maintainable. Finally, we demonstrated how to implement this reusable component in different contexts, such as the `People` and `Countries` tables.

This approach not only saves time but also enhances code readability and maintainability. The reusable component can be extended further to support more advanced features and customizations, which we will cover in part two of this series.

I hope you found this guide helpful and that you learned something new about creating reusable components in React with TypeScript. Please feel free to leave your comments and questions below. Thanks for reading!