DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 963,274 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Cover image for How to Build a Dynamic Table Component in React
Francisco Mendes
Francisco Mendes

Posted on

How to Build a Dynamic Table Component in React

Introduction

I would say that tables are one of the most popular components of web applications and nowadays we have so many Open Source projects that offer solutions for their implementation.

But sometimes the design team can deliver a fully custom design with a good number of features and filters that require a fully custom component/logic.

For these same reasons in today's article I will explain how to create a fully pivot table that is easily extensible.

What are we going to use?

Today we are not going to use many tools out of the ordinary, such as:

  • Stitches - a css-in-js styling library with phenomenal development experience
  • lodash - utility library very easy to use (to work with arrays, objects, strings, etc.)

Bear in mind that although these are the libraries used in this article, the same result is also easily replicable with others.

Prerequisites

To follow this tutorial, you need:

  • Basic understanding of React
  • Basic understanding of TypeScript

You won't have any problem if you don't know TypeScript, you can always "ignore" the data types, however in today's example it makes the whole process much easier.

Getting Started

As a first step, create a project directory and navigate into it:

yarn create vite react-table --template react-ts
cd react-table
Enter fullscreen mode Exit fullscreen mode

Now we can install the necessary dependencies:

yarn add lodash.get @stitches/react @fontsource/anek-telugu
yarn add -D @types/lodash.get
Enter fullscreen mode Exit fullscreen mode

An important point is the definition of the data structure that we are going to use to define the columns of our table and in this same structure we have to specify two properties that I consider important.

One of these properties will be the key which will correspond to the key of the json object of the data that we are going to use to get the value of each of the columns in the table.

The other property that I think is important is actually a function called render() which should be optional, this is because we might want to render a custom component for the column cell and this method is used to render to render that same content when specified.

In addition to this, another important point that I will mention is the fact that the components have to be generic, so that later we can infer data and intelisense terms in the definition of the columns.

// @/src/components/Table.tsx
import { styled } from "@stitches/react";

import { TableHeader } from "./TableHeader";
import { TableRow } from "./TableRow";

export interface IColumnType<T> {
  key: string;
  title: string;
  width?: number;
  render?: (column: IColumnType<T>, item: T) => void;
}

interface Props<T> {
  data: T[];
  columns: IColumnType<T>[];
}

const TableWrapper = styled("table", {
  borderCollapse: "collapse",
  border: "none",
  fontFamily: "Anek Telugu",
});

export function Table<T>({ data, columns }: Props<T>): JSX.Element {
  return (
    <TableWrapper>
      <thead>
        <TableHeader columns={columns} />
      </thead>
      <tbody>
        <TableRow data={data} columns={columns} />
      </tbody>
    </TableWrapper>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the excerpt above we can see that the structure of the columns called IColumnType has been defined, which will have four properties. As were defined the props of the main component of the table, which receives only two props, an array of data and an array of columns.

In this same component we have two components that were imported but not yet created. Starting with the simplest, TableHeader will be responsible for rendering each of the table columns cells, rendering only the column name/title.

// @/src/components/TableHeader.tsx
import { styled } from "@stitches/react";

import { IColumnType } from "./Table";

interface Props<T> {
  columns: IColumnType<T>[];
}

const TableHeaderCell = styled("th", {
  backgroundColor: "#f1f1f1",
  padding: 12,
  fontWeight: 500,
  textAlign: "left",
  fontSize: 14,
  color: "#2c3e50",
  "&:first-child": {
    borderTopLeftRadius: 12,
  },
  "&:last-child": {
    borderTopRightRadius: 12,
  },
});

export function TableHeader<T>({ columns }: Props<T>): JSX.Element {
  return (
    <tr>
      {columns.map((column, columnIndex) => (
        <TableHeaderCell
          key={`table-head-cell-${columnIndex}`}
          style={{ width: column.width }}
        >
          {column.title}
        </TableHeaderCell>
      ))}
    </tr>
  );
}
Enter fullscreen mode Exit fullscreen mode

Another component that we need to create is the TableRow which, as you may have noticed, will be responsible for rendering each of the cells of each row of the table. To do so, it will be necessary to take into account the data that is passed in the props as well as each of the columns.

// @/src/components/TableRow.tsx
import { styled } from "@stitches/react";

import { IColumnType } from "./Table";
import { TableRowCell } from "./TableRowCell";

interface Props<T> {
  data: T[];
  columns: IColumnType<T>[];
}

const TableRowItem = styled("tr", {
  cursor: "auto",
  "&:nth-child(odd)": {
    backgroundColor: "#f9f9f9",
  },
  "&:last-child": {
    borderBottomLeftRadius: 12,
    borderBottomRightRadius: 12,
  },
});

export function TableRow<T>({ data, columns }: Props<T>): JSX.Element {
  return (
    <>
      {data.map((item, itemIndex) => (
        <TableRowItem key={`table-body-${itemIndex}`}>
          {columns.map((column, columnIndex) => (
            <TableRowCell
              key={`table-row-cell-${columnIndex}`}
              item={item}
              column={column}
            />
          ))}
        </TableRowItem>
      ))}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the component above, we map the data with each of the columns, but we still need to create a component called TableRowCell that will be responsible for rendering the content of each of the cells in the row.

// @/src/components/TableRowCell.tsx
import { styled } from "@stitches/react";
import get from "lodash.get";

import { IColumnType } from "./Table";

interface Props<T> {
  item: T;
  column: IColumnType<T>;
}

const TableCell = styled("td", {
  padding: 12,
  fontSize: 14,
  color: "grey",
});

export function TableRowCell<T>({ item, column }: Props<T>): JSX.Element {
  const value = get(item, column.key);
  return (
    <TableCell>{column.render ? column.render(column, item) : value}</TableCell>
  );
}
Enter fullscreen mode Exit fullscreen mode

As you can see, it is a very simple implementation, if the column has the render() function we will render a specific custom component, otherwise we will get the specific value of the column according to the key of the column.

And with that, my dears, we created a table that can be reused as if there were no tomorrow. Now the question remains, how should it be used? It's like this:

// @/src/App.tsx
import "@fontsource/anek-telugu";
import { styled } from "@stitches/react";

import { Table, IColumnType } from "./components";

interface IData {
  fullName: string;
  role: string;
  tags: string[];
}

const Span = styled("span", {
  background: "#596b7e",
  color: "white",
  paddingLeft: 10,
  paddingRight: 10,
  borderRadius: 99999,
});

const columns: IColumnType<IData>[] = [
  {
    key: "fullName",
    title: "Full Name",
    width: 200,
  },
  {
    key: "role",
    title: "Role",
    width: 200,
  },
  {
    key: "tags",
    title: "Tags",
    width: 200,
    render: (_, { tags }) => (
      <>
        {tags.map((tag, tagIndex) => (
          <Span key={`tag-${tagIndex}`} style={{ marginLeft: tagIndex * 4 }}>
            {tag}
          </Span>
        ))}
      </>
    ),
  },
];

const data: IData[] = [
  {
    fullName: "Francisco Mendes",
    role: "Full Stack",
    tags: ["dev", "blogger"],
  },
  {
    fullName: "Ricardo Malva",
    role: "Social Media Manager",
    tags: ["designer", "photographer"],
  },
];

export const App = () => {
  return <Table data={data} columns={columns} />;
};
Enter fullscreen mode Exit fullscreen mode

In the code above we import the Table component as well as the IColumnType data type. Then we define the data types of the data we want to have in the table, which in this case I called IData.

Then I inferred the data type IData in the generic IColumnType and defined each column of the table and as you may have noticed, the column with the key tags will render a custom component and thanks to the inference of data types in the generic we know that in this case it is an array of strings and that we are going to render each one of them in a span.

What are the next challenges?

This point is quite relative, but I would focus on the following features:

  • Allow resizing of columns by updating width value
  • Allow sorting of table rows (asc or desc)
  • Implement row selection

Just one more thing, the table has to be stateless.

Conclusion

As usual, I hope you enjoyed the article and that it helped you with an existing project or simply wanted to try it out.

If you found a mistake in the article, please let me know in the comments so I can correct it. Before finishing, if you want to access the source code of this article, I leave here the link to the github repository.

Top comments (0)

You can see total article reactions, views, and listing information by heading over to your dashboard.