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:
- Search and filtering
- Sorting
- Pagination (e.g., 10, 20, 50, or 100 items per page)
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:
- Accepts any data structure using TypeScript generics.
- Supports text-based filtering.
- Allows column-based sorting (ascending/descending).
- Displays configurable action buttons (e.g., Edit, Delete, View).
- Handles pagination (page number, items per page) via external control.
- Shows a loading state while data is being fetched.
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:
- Avoid repetitive code in each screen that displays a table.
- Ensure consistent UX across different parts of your app.
- Make future improvements or bug fixes easier, since the logic is centralized.
This also encourages better collaboration in teams, especially when multiple developers need to implement data tables quickly and consistently.
🛠️ Tech Stack
- React 18+
- TypeScript
- Mantine UI (for fast styling and accessibility out of the box)
- Icons from Tabler Icons
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:
GenericPaginatedTable.tsx– The main component that renders the UI and manages column rendering and interaction events.GenericPaginatedTable.types.ts– A type definition file that ensures strong typing and flexibility, allowing the component to work with any data model.
📄 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:

