Creating a Reusable Paginated Table Component in React with TypeScript

Creating a Reusable Paginated Table Component in React with TypeScript

2025-07-01

Learn how to build a generic, reusable table component in React with pagination, sorting, and search using Mantine UI and TypeScript.

Throughout my experience as a developer, I've noticed that displaying data in tables is one of the most common patterns in various types of applications. Whether you're building admin dashboards, reporting interfaces, or internal tools, it's very likely you'll need to render a list of records — often retrieved from an API — and enhance the user experience with features like:

When datasets grow to thousands or even millions of records, these functionalities are not just nice-to-have — they’re essential for usability and performance.

However, in many projects — especially legacy codebases or when working with multiple developers — I've observed a recurring pattern: the same table logic gets copied and pasted multiple times, often without abstraction or consideration for reusability. This leads to duplicated code, inconsistent behavior, and more maintenance overhead.

To address this, I built a flexible and reusable solution that has worked well for me across multiple projects. In this post, I’ll walk you through the design and implementation of a generic paginated table component in React using TypeScript.


🚀 What Are We Building?

We’re building a GenericPaginatedTable<T> component that:

This component is especially useful for dashboards, admin panels, or data-heavy interfaces, and follows the "dumb/smart components" pattern — where the table itself is presentational (dumb), and the logic (like fetching data or reacting to filters) is handled outside.


💡 Why This Approach?

By abstracting pagination, sorting, and filtering into a generic component, you:

This also encourages better collaboration in teams, especially when multiple developers need to implement data tables quickly and consistently.


🛠️ Tech Stack

Although this example uses Mantine, the logic and structure can be adapted easily to work with any other UI library like Material UI, Chakra UI, or even Tailwind + Headless UI.


🧱 Component Structure

The component is broken into two main parts:


📄 Type Definitions

export interface TableRow {{
  id: string;
}}

export interface TableColumn<T> {{
  field: keyof T;
  header: string;
  formatter?: (value: T[keyof T], row: T) => React.ReactNode;
}}

These types allow you to define columns like:

columns: [
  {{ field: 'name', header: 'Name' }},
  {{ field: 'createdAt', header: 'Created At', formatter: formatDate }}
]

And actions like:

actions: [
  {{ type: 'Edit', icon: <IconEdit />, tooltip: 'Edit' }},
  {{ type: 'Delete', icon: <IconTrash />, tooltip: 'Delete', disabled: row => row.status === 'archived' }}
]

🧠 How It Works

🔍 Search

<TextInput
  placeholder="Search..."
  value={search}
  onChange={handleSearchChange}
/>

Triggers the onQueryChange with updated query values.

🔃 Sorting

<Table.Th onClick={() => handleSort(column.field)}>
  {column.header}
  {sortField === column.field && (sortOrder === 'asc' ? <IconArrowUp /> : <IconArrowDown />)}
</Table.Th>

Toggles sorting state and updates the query.

📄 Table Body & Actions

{data.map(row => (
  <Table.Tr key={row.id}>
    {columns.map(col => (
      <Table.Td>{col.formatter ? col.formatter(row[col.field], row) : row[col.field]}</Table.Td>
    ))}
    <Table.Td>
      {actions.map(action => (
        <ActionIcon onClick={() => onAction({{ type: action.type, row }})}>
          {action.icon}
        </ActionIcon>
      ))}
    </Table.Td>
  </Table.Tr>
))}

📦 Pagination

<Pagination
  value={pagination.page}
  onChange={handlePageChange}
  total={pagination.totalPages}
/>

📦 Code & Demo

You can explore the code and play with the component here:

Christian Rios

Christian Rios

Full-stack developer writing about React, Angular, .NET, architecture, and delivery.