https://github.com/jacksonkasi1/tnks-data-table
Advanced data table component with server-side operations using Hono.js, Drizzle ORM, and PostgreSQL
https://github.com/jacksonkasi1/tnks-data-table
data-table drizzle filtering hono nextjs postgresql react server-side-pagination shadcn-ui sorting tailwindcss tailwindcss-v4 typescript ui-component zod
Last synced: 16 days ago
JSON representation
Advanced data table component with server-side operations using Hono.js, Drizzle ORM, and PostgreSQL
- Host: GitHub
- URL: https://github.com/jacksonkasi1/tnks-data-table
- Owner: jacksonkasi1
- License: mit
- Created: 2025-04-08T05:51:43.000Z (24 days ago)
- Default Branch: main
- Last Pushed: 2025-04-15T05:33:57.000Z (17 days ago)
- Last Synced: 2025-04-15T06:28:40.124Z (17 days ago)
- Topics: data-table, drizzle, filtering, hono, nextjs, postgresql, react, server-side-pagination, shadcn-ui, sorting, tailwindcss, tailwindcss-v4, typescript, ui-component, zod
- Language: TypeScript
- Homepage: https://tnks-data-table.vercel.app/
- Size: 550 KB
- Stars: 2
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- awesome-shadcn-ui - Link - 04-19 | (Libs and Components)
README
# Advanced Data Table Component Documentation
**Version:** 1.0.0
**Updated:** 2025-04-14
**Author:** Jackson Kasi## Table of Contents
1. [Introduction](#introduction)
2. [Features Overview](#features-overview)
3. [File Structure](#file-structure)
4. [Installation & Setup](#installation--setup)
5. [Basic Usage](#basic-usage)
6. [Core Components](#core-components)
7. [API Integration](#api-integration)
8. [Advanced Configuration](#advanced-configuration)
- [Column Configuration](#column-configuration)
- [Row Actions](#row-actions)
- [Filtering & Sorting](#filtering--sorting)
- [Pagination](#pagination)
- [Date Range Filtering](#date-range-filtering)
- [Row Selection](#row-selection)
- [Toolbar Customization](#toolbar-customization)
- [Export Options](#export-options)
9. [Server Implementation](#server-implementation)
- [API Endpoints](#api-endpoints)
- [Request & Response Formats](#request--response-formats)
- [Error Handling](#error-handling)
10. [Popups & Modals](#popups--modals)
11. [Customization](#customization)
12. [Performance Optimization](#performance-optimization)
13. [Best Practices](#best-practices)
14. [Troubleshooting](#troubleshooting)
15. [Complete API Reference](#complete-api-reference)
16. [Example Implementations](#example-implementations)---
## Introduction
The Advanced Data Table component is a highly configurable and feature-rich table implementation built on top of Shadcn UI components and TanStack Table (React Table v8). It's designed to handle enterprise-level requirements including complex data operations, server-side processing, and customizable UI elements.
This documentation provides comprehensive guidance on how to implement, configure, and extend the data table for your specific needs.
## Server
Check out the API development document to understand the default configuration for this table. [π Click here](./src/SERVER.md)
### Key Benefits
- **TypeScript Support**: Fully typed components for better developer experience
- **Modular Architecture**: Easily extendable and customizable
- **Server Integration**: Built-in support for server-side operations
- **Accessibility**: Follows WCAG guidelines for accessible tables
- **Performance Optimized**: Efficient rendering even with large datasets
- **Responsive Design**: Works across various screen sizes
- **Theming Support**: Customizable appearance with Tailwind CSS---
## Features Overview
The Data Table includes the following features:
### Data Management
- β Server-side pagination
- β Server-side sorting
- β Server-side filtering
- β Single & multi-row selection
- β Optimistic UI updates### UI Features
- β Responsive layout
- β Column resizing
- β Column visibility toggle
- β Date range filtering
- β Search functionality
- β Customizable toolbar
- β Row actions menu
- β Bulk action support### Operations
- β Add new records
- β Edit existing records
- β Delete single records
- β Bulk delete operations
- β Data export (CSV/Excel)### Integration
- β React Query data fetching
- β Zod validation
- β Form handling with React Hook Form
- β Toast notifications
- β URL state persistence---
## File Structure
The data table implementation follows a modular structure to separate concerns and improve maintainability. Below is the recommended file structure for implementing the data table in your project:
```sh
src/
βββ api/ # API integration layer
β βββ entity/ # Entity-specific API functions
β βββ add-entity.ts # Create operation
β βββ delete-entity.ts # Delete operation
β βββ fetch-entities.ts # List operation with filters
β βββ fetch-entity-by-ids.ts # Fetch specific entities
β
βββ components/ # Shared UI components
βββ πdata-table # Core data table components
βββ πhooks # Custom React hooks for data-table
βββ use-table-column-resize.ts # Hook for managing column resize state and persistence
βββ πutils # Utility functions and helpers
βββ column-sizing.ts # Functions for calculating and managing column widths
βββ conditional-state.ts # Logic for conditional rendering and state transitions
βββ date-format.ts # Date formatting and manipulation utilities
βββ deep-utils.ts # Deep object comparison and manipulation
βββ export-utils.ts # Utilities for data export (CSV/Excel)
βββ index.ts # Export barrel file for utilities
βββ keyboard-navigation.ts # Keyboard navigation and accessibility
βββ search.ts # Search functionality and text matching
βββ table-config.ts # Table configuration types and defaults
βββ table-state-handlers.ts # Handlers for table state changes
βββ url-state.ts # URL state persistence utilities
βββ column-header.tsx # Sortable column header component
βββ data-export.tsx # Component for export functionality UI
βββ data-table-resizer.tsx # Column resize handler component
βββ data-table.tsx # Main data table component
βββ pagination.tsx # Pagination controls component
βββ toolbar.tsx # Table toolbar with search and filtering
βββ view-options.tsx # Column visibility and display options
β
βββ app/ # Application routes and pages
β βββ (section)/ # Section grouping
β βββ entity-table/ # Entity-specific implementation
β βββ components/ # Entity table components
β β βββ columns.tsx # Column definitions
β β βββ row-actions.tsx # Row action menu
β β βββ toolbar-options.tsx # Toolbar customizations
β β βββ actions/ # Action components
β β βββ add-entity-popup.tsx # Add modal
β β βββ delete-entity-popup.tsx # Delete confirmation
β β βββ bulk-delete-popup.tsx # Bulk delete confirmation
β βββ schema/ # Data schemas
β β βββ entity-schema.ts # Entity type definitions
β β βββ index.ts # Schema exports
β βββ utils/ # Utility functions
β β βββ config.ts # Table configuration
β β βββ data-fetching.ts # Data fetching hooks
β βββ index.tsx # Table component entry
```### Understanding the Structure
This file structure follows a clear separation of concerns:
1. **API Layer**: Handles all communication with the backend
2. **Core Components**: Reusable data table building blocks
3. **Implementation**: Entity-specific configuration and customization
4. **Schema**: Type definitions and validation
5. **Utils**: Helper functions for specific implementationsBy following this structure, you can easily maintain and extend your data tables while keeping each part focused on its specific responsibility.
---
## Installation & Setup
### Prerequisites
- Next.js 13+ with App Router
- React 18+
- TypeScript 5+
- Tailwind CSS
- Shadcn UI components### Installation Steps
1. **Install required dependencies**:
```bash
# Installing dependencies with Bun
bun add @tanstack/react-table @tanstack/react-query zod @hookform/resolvers sonner date-fns
```2. **Copy the core data table components** to your project:
Create a `/components/data-table` directory in your project and copy the following core components:
- `data-table.tsx`: Main component
- `column-header.tsx`: Sortable column headers
- `filters.tsx`: Filter components
- `utils.ts`: Helper functions3. **Set up the API layer**:
Create an API directory structure as shown in the file structure section above. Implement the necessary API functions for your entity.
4. **Create Schema Definitions**:
Define your entity schema using Zod for type validation.
5. **Implement the Data Table**:
Create your entity-specific table implementation following the structure outlined above.
---
## Basic Usage
Here's a basic example of how to implement a data table for your entity:
### 1. Define your entity schema
```typescript
// src/app/(section)/entity-table/schema/entity-schema.ts
import { z } from "zod";export const entitySchema = z.object({
id: z.number(),
name: z.string(),
email: z.string(),
created_at: z.string(),
// Add other fields as needed
});export type Entity = z.infer;
export const entitiesResponseSchema = z.object({
success: z.boolean(),
data: z.array(entitySchema),
pagination: z.object({
page: z.number(),
limit: z.number(),
total_pages: z.number(),
total_items: z.number(),
}),
});
```### 2. Create API functions
```typescript
// src/api/entity/fetch-entities.ts
import { z } from "zod";
import { entitiesResponseSchema } from "@/app/(section)/entity-table/schema";const API_BASE_URL = "/api";
export async function fetchEntities({
search = "",
from_date = "",
to_date = "",
sort_by = "created_at",
sort_order = "desc",
page = 1,
limit = 10,
}) {
// Build query parameters
const params = new URLSearchParams();
if (search) params.append("search", search);
if (from_date) params.append("from_date", from_date);
if (to_date) params.append("to_date", to_date);
params.append("sort_by", sort_by);
params.append("sort_order", sort_order);
params.append("page", page.toString());
params.append("limit", limit.toString());// Fetch data
const response = await fetch(`${API_BASE_URL}/entities?${params.toString()}`);if (!response.ok) {
throw new Error(`Failed to fetch entities: ${response.statusText}`);
}const data = await response.json();
return entitiesResponseSchema.parse(data);
}
```### 3. Create a data fetching hook
```typescript
// src/app/(section)/entity-table/utils/data-fetching.ts
import { useQuery, keepPreviousData } from "@tanstack/react-query";
import { fetchEntities } from "@/api/entity/fetch-entities";export function useEntitiesData(
page: number,
pageSize: number,
search: string,
dateRange: { from_date: string; to_date: string },
sortBy: string,
sortOrder: string
) {
return useQuery({
queryKey: [
"entities",
page,
pageSize,
search,
dateRange,
sortBy,
sortOrder,
],
queryFn: () =>
fetchEntities({
page,
limit: pageSize,
search,
from_date: dateRange.from_date,
to_date: dateRange.to_date,
sort_by: sortBy,
sort_order: sortOrder,
}),
placeholderData: keepPreviousData,
});
}// Add this property for the DataTable component
useEntitiesData.isQueryHook = true;
```### 4. Define your columns
```typescript
// src/app/(section)/entity-table/components/columns.tsx
"use client";import { format } from "date-fns";
import { ColumnDef } from "@tanstack/react-table";// Import components
import { DataTableColumnHeader } from "@/components/data-table/column-header";
import { Checkbox } from "@/components/ui/checkbox";// Import schema and actions
import { Entity } from "../schema";
import { DataTableRowActions } from "./row-actions";export const getColumns = (
handleRowDeselection: ((rowId: string) => void) | null | undefined
): ColumnDef[] => {
const baseColumns: ColumnDef[] = [
{
accessorKey: "name",
header: ({ column }) => (
),
cell: ({ row }) => (
{row.getValue("name")}
),
size: 200,
},
{
accessorKey: "email",
header: ({ column }) => (
),
cell: ({ row }) =>{row.getValue("email")},
size: 250,
},
{
accessorKey: "created_at",
header: ({ column }) => (
),
cell: ({ row }) => {
const date = new Date(row.getValue("created_at"));
const formatted = format(date, "MMM d, yyyy");
return{formatted};
},
size: 120,
},
{
id: "actions",
header: ({ column }) => (
),
cell: ({ row, table }) => ,
size: 100,
},
];// Only include selection column if row selection is enabled
if (handleRowDeselection !== null) {
return [
{
id: "select",
header: ({ table }) => (
table.toggleAllPageRowsSelected(!!value)
}
aria-label="Select all"
className="translate-y-0.5 cursor-pointer"
/>
),
cell: ({ row }) => (
{
row.toggleSelected(!!value);
if (!value && handleRowDeselection) {
handleRowDeselection(row.id);
}
}}
aria-label="Select row"
className="translate-y-0.5 cursor-pointer"
/>
),
enableSorting: false,
enableHiding: false,
size: 50,
},
...baseColumns,
];
}return baseColumns;
};
```### 5. Implement the main table component
```typescript
// src/app/(section)/entity-table/index.tsx
"use client";import { DataTable } from "@/components/data-table/data-table";
import { getColumns } from "./components/columns";
import { useExportConfig } from "./utils/config";
import { fetchEntitiesByIds } from "@/api/entity/fetch-entities-by-ids";
import { useEntitiesData } from "./utils/data-fetching";
import { ToolbarOptions } from "./components/toolbar-options";
import { Entity } from "./schema";export default function EntityTable() {
return (
getColumns={getColumns}
exportConfig={useExportConfig()}
fetchDataFn={useEntitiesData}
fetchByIdsFn={fetchEntitiesByIds}
idField="id"
pageSizeOptions={[10, 20, 50, 100]}
renderToolbarContent={({
selectedRows,
allSelectedIds,
totalSelectedCount,
resetSelection,
}) => (
({
id: row.id,
name: row.name,
}))}
allSelectedIds={allSelectedIds}
totalSelectedCount={totalSelectedCount}
resetSelection={resetSelection}
/>
)}
config={{
enableRowSelection: true,
enableSearch: true,
enableDateFilter: true,
enableColumnVisibility: true,
enableUrlState: true,
columnResizingTableId: "entity-table",
}}
/>
);
}
```### 6. Add the table to your page
```typescript
// src/app/(section)/entities/page.tsx
import { Metadata } from "next";
import { Suspense } from "react";
import EntityTable from "./entity-table";export const metadata: Metadata = {
title: "Entities Management",
};export default function EntitiesPage() {
return (
Entities List
Loading...}>
);
}
```---
## Core Components
The data table is built using several key components that work together. Understanding these components will help you customize and extend the table according to your needs.
### Main Data Table Component
The `DataTable` component is the main entry point for the table implementation. It handles:
- State management
- Data fetching
- URL state persistence
- Pagination
- Sorting
- Filtering
- Row selection
- Export functionality### Column Header Component
The `DataTableColumnHeader` component provides:
- Visual indication of sort direction
- Sorting controls
- Column header rendering### Filter Components
Filter components provide UI for filtering data:
- Search input
- Date range picker
- Custom filters can be added### Row Actions
Row action components handle operations on individual rows:
- Action menus
- Delete confirmations
- Edit forms### Toolbar Components
The toolbar area provides:
- Global actions (add new, bulk delete)
- Filter controls
- Export buttons
- View options---
## API Integration
### API Layer Structure
The data table relies on a consistent API layer to communicate with your backend services. Each entity should have the following API functions:
#### 1. Fetch List with Filtering
```typescript
// Function signature
async function fetchEntities({
search?: string,
from_date?: string,
to_date?: string,
sort_by?: string,
sort_order?: string,
page?: number,
limit?: number,
}): Promise;
```#### 2. Fetch Multiple by IDs
```typescript
// Function signature
async function fetchEntitiesByIds(ids: number[]): Promise;
```#### 3. Add Entity
```typescript
// Function signature
async function addEntity(data: NewEntity): Promise;
```#### 4. Delete Entity
```typescript
// Function signature
async function deleteEntity(id: number): Promise;
```### Error Handling in API Layer
Each API function should include proper error handling:
```typescript
export async function addEntity(
entityData: NewEntity
): Promise {
try {
// Validate input data
const validatedData = addEntitySchema.parse(entityData);// Make API request
const response = await fetch(`${API_BASE_URL}/entities/add`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(validatedData),
});// Parse response
const data = await response.json();// Validate response
const validatedResponse = addEntityResponseSchema.parse(data);// Check if the request was successful
if (!response.ok) {
throw new Error(validatedResponse.error || "Failed to add entity");
}return validatedResponse;
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error("Invalid response format from server");
}
throw error;
}
}
```---
## Advanced Configuration
### Column Configuration
Columns are defined using TanStack Table's `ColumnDef` interface. Each column can be customized with:
#### Basic Properties
- `accessorKey`: The key to use when accessing the data
- `header`: Custom header rendering
- `cell`: Custom cell rendering
- `size`: Column width#### Advanced Properties
- `enableSorting`: Enable/disable sorting for this column
- `enableHiding`: Allow column to be hidden/shown
- `meta`: Custom metadata for the column
- `filterFn`: Custom filtering function#### Example Column Definition
```typescript
{
accessorKey: "name",
header: ({ column }) => (
),
cell: ({ row }) => {
// Custom rendering with additional styling or components
return (
{row.getValue("name")}
);
},
enableSorting: true,
enableHiding: true,
size: 200,
}
```### Row Actions
Row actions provide operations on individual rows. They're implemented using the `DataTableRowActions` component:
```typescript
// src/app/(section)/entity-table/components/row-actions.tsx
"use client";import * as React from "react";
import { DotsHorizontalIcon } from "@radix-ui/react-icons";
import { Row } from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { entitySchema } from "../schema";
import { DeleteEntityPopup } from "./actions/delete-entity-popup";interface DataTableRowActionsProps {
row: Row;
table: any; // Table instance
}export function DataTableRowActions({
row,
table,
}: DataTableRowActionsProps) {
const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false);
const entity = entitySchema.parse(row.original);// Function to reset all selections
const resetSelection = () => {
table.resetRowSelection();
};return (
<>
Open menu
console.log("Edit", entity)}>
Edit
View Details
setDeleteDialogOpen(true)}>
Delete
>
);
}
```### Filtering & Sorting
The data table supports server-side filtering and sorting. Configure the API to handle the following parameters:
- `search`: Text search term
- `sort_by`: Column to sort by
- `sort_order`: Sort direction (asc/desc)### Pagination
Server-side pagination is handled through the following parameters:
- `page`: Current page number (1-based)
- `limit`: Number of items per pageThe server should return pagination information in the response:
```json
{
"success": true,
"data": [...],
"pagination": {
"page": 1,
"limit": 10,
"total_pages": 5,
"total_items": 48
}
}
```### Date Range Filtering
Date range filtering allows filtering records by a date field:
- `from_date`: Start date in ISO format
- `to_date`: End date in ISO formatThis is useful for limiting records to a specific time period.
### Row Selection
Row selection enables operations on multiple rows. It's controlled by:
```typescript
config={{
enableRowSelection: true, // Enable row selection
enableClickRowSelect: false // Enable/disable row selection by clicking anywhere in the row
}}
```Selected rows can be accessed via the `renderToolbarContent` prop:
```typescript
renderToolbarContent={({
selectedRows, // Currently visible selected rows
allSelectedIds, // All selected IDs across pages
totalSelectedCount, // Total number of selected items
resetSelection // Function to reset selection
}) => (
)}
```### Toolbar Customization
The toolbar area can be customized with your own components:
```typescript
// src/app/(section)/entity-table/components/toolbar-options.tsx
"use client";import * as React from "react";
import { Button } from "@/components/ui/button";
import { AddEntityPopup } from "./actions/add-entity-popup";
import { BulkDeletePopup } from "./actions/bulk-delete-popup";interface ToolbarOptionsProps {
selectedEntities: { id: number; name: string }[];
allSelectedIds?: number[];
totalSelectedCount: number;
resetSelection: () => void;
}export const ToolbarOptions = ({
selectedEntities,
allSelectedIds = [],
totalSelectedCount,
resetSelection,
}: ToolbarOptionsProps) => {
const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false);return (
{totalSelectedCount > 0 && (
<>
setDeleteDialogOpen(true)}
>
Delete ({totalSelectedCount})
>
)}
);
};
```### Export Options
The data table supports exporting data in various formats. Configure export options:
```typescript
// src/app/(section)/entity-table/utils/config.ts
import { useMemo } from "react";export function useExportConfig() {
// Column mapping for export
const columnMapping = useMemo(() => {
return {
id: "ID",
name: "Name",
email: "Email",
created_at: "Created Date",
// Add other fields
};
}, []);// Column widths for Excel export
const columnWidths = useMemo(() => {
return [
{ wch: 10 }, // ID
{ wch: 20 }, // Name
{ wch: 30 }, // Email
{ wch: 20 }, // Created At
];
}, []);// Headers for CSV export
const headers = useMemo(() => {
return ["id", "name", "email", "created_at"];
}, []);return {
columnMapping,
columnWidths,
headers,
entityName: "entities", // Used in filename
};
}
```---
## Server Implementation
### API Endpoints
The data table expects the following API endpoints:
#### 1. List endpoint
```
GET /api/entities
```Parameters:
- `search` (optional): Search term
- `from_date` (optional): Start date filter
- `to_date` (optional): End date filter
- `sort_by` (optional): Column to sort by
- `sort_order` (optional): 'asc' or 'desc'
- `page` (optional): Page number (default: 1)
- `limit` (optional): Items per page (default: 10)#### 2. Create endpoint
```
POST /api/entities/add
```Body: Entity data according to schema
#### 3. Delete endpoint
```
DELETE /api/entities/:id
```Path parameter: `id` - Entity ID
### Request & Response Formats
#### List Response Format
```json
{
"success": true,
"data": [
{
"id": 1,
"name": "Example Entity",
"email": "[email protected]",
"created_at": "2025-01-15T10:30:00Z",
...
},
...
],
"pagination": {
"page": 1,
"limit": 10,
"total_pages": 5,
"total_items": 48
}
}
```#### Create Request Format
```json
{
"name": "New Entity",
"email": "[email protected]",
...
}
```#### Create Response Format
```json
{
"success": true,
"data": {
"id": 49,
"name": "New Entity",
"email": "[email protected]",
"created_at": "2025-04-14T06:44:16Z",
...
}
}
```#### Delete Response Format
```json
{
"success": true,
"message": "Entity deleted successfully"
}
```### Error Handling
All API responses should follow a consistent error format:
```json
{
"success": false,
"error": "Error message",
"details": [] // Optional array with detailed error information
}
```HTTP status codes should also be appropriate:
- 400: Bad Request (validation errors)
- 404: Not Found
- 409: Conflict (e.g., duplicate entity)
- 500: Internal Server Error---
## Popups & Modals
The data table uses several modal dialogs for different operations. Here's how to implement them:
### Add Entity Popup
```tsx
// src/app/(section)/entity-table/components/actions/add-entity-popup.tsx
"use client";import * as React from "react";
import { useRouter } from "next/navigation";
import { z } from "zod";
import { toast } from "sonner";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQueryClient } from "@tanstack/react-query";import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
} from "@/components/ui/dialog";
import {
Form,
FormField,
FormItem,
FormLabel,
FormControl,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";// Import API
import { addEntity } from "@/api/entity/add-entity";// Form schema
const formSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email format"),
// Add other fields
});type FormValues = z.infer;
export function AddEntityPopup() {
const router = useRouter();
const [open, setOpen] = React.useState(false);
const [isLoading, setIsLoading] = React.useState(false);
const queryClient = useQueryClient();const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
email: "",
},
});const onSubmit = async (data: FormValues) => {
try {
setIsLoading(true);
const response = await addEntity(data);if (response.success) {
toast.success("Entity added successfully");
form.reset();
setOpen(false);
router.refresh();
await queryClient.invalidateQueries({ queryKey: ["entities"] });
} else {
toast.error(response.error || "Failed to add entity");
}
} catch (error) {
toast.error(
error instanceof Error ? error.message : "Failed to add entity"
);
} finally {
setIsLoading(false);
}
};return (
Add Entity
Add New Entity
(
Name
)}
/>
(
)}
/>
{/* Add other form fields */}
setOpen(false)}
disabled={isLoading}
>
Cancel
{isLoading ? "Adding..." : "Add Entity"}
);
}
```### Delete Entity Popup
```tsx
// src/app/(section)/entity-table/components/actions/delete-entity-popup.tsx
"use client";import * as React from "react";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { useQueryClient } from "@tanstack/react-query";import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";// Import API
import { deleteEntity } from "@/api/entity/delete-entity";interface DeleteEntityPopupProps {
open: boolean;
onOpenChange: (open: boolean) => void;
entityId: number;
entityName: string;
resetSelection?: () => void;
}export function DeleteEntityPopup({
open,
onOpenChange,
entityId,
entityName,
resetSelection,
}: DeleteEntityPopupProps) {
const router = useRouter();
const queryClient = useQueryClient();
const [isLoading, setIsLoading] = React.useState(false);const handleDelete = async () => {
try {
setIsLoading(true);
const response = await deleteEntity(entityId);if (response.success) {
toast.success("Entity deleted successfully");
onOpenChange(false);// Reset the selection state if the function is provided
if (resetSelection) {
resetSelection();
}// Refresh data
router.refresh();
await queryClient.invalidateQueries({ queryKey: ["entities"] });
} else {
toast.error(response.error || "Failed to delete entity");
}
} catch (error) {
toast.error(
error instanceof Error ? error.message : "Failed to delete entity"
);
} finally {
setIsLoading(false);
}
};return (
Delete Entity
Are you sure you want to delete {entityName}? This action cannot be
undone.
onOpenChange(false)}
disabled={isLoading}
>
Cancel
{isLoading ? "Deleting..." : "Delete"}
);
}
```### Bulk Delete Popup
```tsx
// src/app/(section)/entity-table/components/actions/bulk-delete-popup.tsx
"use client";import * as React from "react";
import { useRouter } from "next/navigation";
import { useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";// Import API
import { deleteEntity } from "@/api/entity/delete-entity";interface BulkDeletePopupProps {
open: boolean;
onOpenChange: (open: boolean) => void;
selectedEntities: { id: number; name: string }[];
allSelectedIds?: number[];
totalSelectedCount?: number;
resetSelection: () => void;
}export function BulkDeletePopup({
open,
onOpenChange,
selectedEntities,
allSelectedIds,
totalSelectedCount,
resetSelection,
}: BulkDeletePopupProps) {
const router = useRouter();
const queryClient = useQueryClient();
const [isLoading, setIsLoading] = React.useState(false);// Use allSelectedIds if available, otherwise fallback to selectedEntities ids
const idsToDelete =
allSelectedIds || selectedEntities.map((entity) => entity.id);// Use total count if available, otherwise fallback to visible items count
const itemCount = totalSelectedCount ?? selectedEntities.length;const handleDelete = async () => {
try {
setIsLoading(true);// Delete entities sequentially
for (const id of idsToDelete) {
const response = await deleteEntity(id);
if (!response.success) {
throw new Error(`Failed to delete entity ID ${id}`);
}
}toast.success(
itemCount === 1
? "Entity deleted successfully"
: `${itemCount} entities deleted successfully`
);onOpenChange(false);
resetSelection();
router.refresh();
await queryClient.invalidateQueries({ queryKey: ["entities"] });
} catch (error) {
toast.error(
error instanceof Error ? error.message : "Failed to delete entities"
);
} finally {
setIsLoading(false);
}
};const getDialogTitle = () => {
if (itemCount === 1) {
return "Delete Entity";
}
return "Delete Entities";
};const getDialogDescription = () => {
if (itemCount === 1 && selectedEntities.length === 1) {
return `Are you sure you want to delete ${selectedEntities[0].name}? This action cannot be undone.`;
}
return `Are you sure you want to delete ${itemCount} entities? This action cannot be undone.`;
};return (
{getDialogTitle()}
{getDialogDescription()}
onOpenChange(false)}
disabled={isLoading}
>
Cancel
{isLoading ? "Deleting..." : "Delete"}
);
}
```---
## Customization
### Custom Column Rendering
You can customize column rendering with custom cell components:
```typescript
{
accessorKey: "status",
header: ({ column }) => (
),
cell: ({ row }) => {
const status = row.getValue("status") as string;// Map status to badge variant
const variant = {
active: "success",
inactive: "secondary",
pending: "warning",
error: "destructive",
}[status] || "outline";return (
{status}
);
},
size: 100,
}
```### Custom Toolbar Content
Add your own content to the toolbar:
```tsx
renderToolbarContent={({
selectedRows,
allSelectedIds,
totalSelectedCount,
resetSelection
}) => (
{totalSelectedCount > 0 && (
<>
{/* Custom bulk action button */}
handleBulkApprove(allSelectedIds)}
>
Approve ({totalSelectedCount})
{/* Default delete button */}
setDeleteDialogOpen(true)}
>
Delete ({totalSelectedCount})
>
)}
)}
```### Custom Filtering
Add custom filtering controls:
```tsx
// Inside your DataTable component
const renderFilters = () => (
All Status
Active
Inactive
Pending
{/* Use custom filters in your API call */}
{statusFilter !== "all" && (
Status: {statusFilter}
setStatusFilter("all")}
/>
)}
);
```### Styling
The data table uses Tailwind CSS for styling. You can customize the appearance:
```tsx
```
---
## Performance Optimization
### Server-Side Operations
For optimal performance, ensure that all data operations are handled server-side:
- Filtering
- Sorting
- PaginationThis approach ensures that:
1. Only necessary data is transferred
2. The client doesn't need to process large datasets
3. Performance scales with your server capacity### Data Batching
When fetching individual records (like for selection across pages), use batching:
```typescript
export async function fetchEntitiesByIds(
entityIds: number[]
): Promise {
if (entityIds.length === 0) {
return [];
}// Use batching to avoid URL length limits
const BATCH_SIZE = 50;
const results: Entity[] = [];// Process in batches
for (let i = 0; i < entityIds.length; i += BATCH_SIZE) {
const batchIds = entityIds.slice(i, i + BATCH_SIZE);try {
const params = new URLSearchParams();
batchIds.forEach((id) => {
params.append("id", id.toString());
});const response = await fetch(
`${API_BASE_URL}/entities?${params.toString()}`
);if (!response.ok) {
throw new Error(`Failed to fetch entities: ${response.statusText}`);
}const data = await response.json();
const parsedData = entitiesResponseSchema.parse(data);results.push(...parsedData.data);
} catch (error) {
console.error(`Error fetching batch of entities:`, error);
}
}return results;
}
```### Query Caching
The data table uses React Query for data fetching, which provides:
- Automatic caching
- Background refetching
- Stale data managementTo optimize React Query usage:
```typescript
useQuery({
queryKey: ["entities", ...], // Include all filters in the queryKey
queryFn: () => fetchEntities({...}),
placeholderData: keepPreviousData, // Show previous data while loading new data
staleTime: 30000, // Data is considered fresh for 30 seconds
refetchOnWindowFocus: false, // Disable refetching when window regains focus
})
```### Virtualized Rendering
For very large tables, consider adding virtualization:
```tsx
import { useVirtualizer } from "@tanstack/react-virtual";// Inside your component:
const tableContainerRef = React.useRef(null);const rowVirtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => tableContainerRef.current,
estimateSize: () => 35, // Approximate row height
overscan: 10,
});// Use with your table:
;
{/* ... */}
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const row = rows[virtualRow.index];
return (
{/* Render cells */}
);
})}
```---
## Best Practices
### 1. State Management
- Use URL state for filters, sorting, and pagination to enable bookmarking and sharing
- Keep complex state in React Query for automatic caching and refetching
- Use local state only for UI-specific state like modal visibility### 2. Error Handling
- Implement consistent error handling across all API calls
- Use toast notifications for user feedback
- Log errors to the console for debugging
- Include error details in the UI when appropriate### 3. Loading States
- Show loading indicators for initial load and filtering operations
- Use optimistic updates for better UX during create/update/delete operations
- Keep previous data visible while loading new data### 4. Accessibility
- Use proper ARIA attributes for interactive elements
- Ensure keyboard navigation works for all table interactions
- Maintain sufficient color contrast for all text
- Make sure all interactive elements have accessible labels### 5. Form Validation
- Use Zod for consistent validation on both client and server
- Provide clear error messages for validation failures
- Validate form inputs as the user types for immediate feedback
- Debounce validation to avoid excessive processing### 6. API Design
- Use consistent API response formats across all endpoints
- Include appropriate HTTP status codes
- Validate input on both client and server
- Use pagination for all list endpoints### 7. Performance
- Only fetch the data you need
- Use pagination for large datasets
- Implement caching for frequently accessed data
- Optimize server queries (use indexes, limit fields, etc.)### 8. Security
- Validate all user inputs (client and server)
- Implement proper authentication and authorization
- Use HTTPS for all API requests
- Sanitize data before displaying it in the UI### 9. Code Organization
- Follow the file structure outlined in this documentation
- Keep components focused on a single responsibility
- Extract reusable logic into custom hooks
- Use consistent naming conventions### 10. Testing
- Write unit tests for critical components
- Test edge cases (empty states, error states, etc.)
- Consider using integration tests for complete workflows
- Test accessibility with automated tools---
## Troubleshooting
### Common Issues and Solutions
#### Issue: Table data doesn't update after adding or deleting items
**Possible Causes:**
- React Query cache not invalidated
- Missing router.refresh() call**Solution:**
```typescript
// After successful mutation:
await queryClient.invalidateQueries({ queryKey: ["entities"] });
router.refresh();
```#### Issue: Form validation errors not displaying
**Possible Causes:**
- Missing FormMessage component
- Incorrect field names in form**Solution:**
```tsx
(
{/* Make sure to include this */}
)}
/>
```#### Issue: Row selection not working correctly across pages
**Possible Causes:**
- Missing handleRowDeselection function
- Not tracking selected rows across pages**Solution:**
```typescript
// In your DataTable component:
const [selectedRowIds, setSelectedRowIds] = React.useState>({});// Pass this to the getColumns function:
getColumns={(handleRowDeselection) => columns(handleRowDeselection)}// Define handleRowDeselection:
const handleRowDeselection = (rowId: string) => {
setSelectedRowIds((prev) => {
const newSelected = { ...prev };
delete newSelected[rowId];
return newSelected;
});
};
```#### Issue: API calls failing with validation errors
**Possible Causes:**
- Schema mismatch between client and server
- Missing required fields
- Format errors (e.g., date format)**Solution:**
- Compare client and server schemas
- Check API request/response in browser devtools
- Add more detailed error reporting from your API#### Issue: "TypeError: Cannot read property 'x' of undefined"
**Possible Causes:**
- Trying to access nested properties that may not exist
- Data structure mismatch**Solution:**
Use optional chaining and nullish coalescing:```typescript
// Instead of data.user.name (which may fail if user is undefined)
const userName = data?.user?.name ?? "Unknown";// Or with array access
const firstItem = data?.items?.[0]?.title ?? "No items";
```---
## Complete API Reference
### DataTable Component Props
| Prop | Type | Required | Default | Description |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------- | -------------------------------------------- |
| `getColumns` | `(handleRowDeselection?: (rowId: string) => void) => ColumnDef[]` | Yes | - | Function to get column definitions |
| `fetchDataFn` | `(page: number, pageSize: number, search: string, dateRange: DateRange, sortBy: string, sortOrder: string) => QueryObserverResult` | Yes | - | Function to fetch data |
| `fetchByIdsFn` | `(ids: number[]) => Promise` | No | - | Function to fetch entities by IDs |
| `idField` | `keyof T` | Yes | - | Field to use as unique identifier |
| `pageSizeOptions` | `number[]` | No | `[10, 20, 30, 50, 100]` | Available page size options |
| `renderToolbarContent` | `(options: ToolbarOptions) => React.ReactNode` | No | - | Function to render custom toolbar content |
| `exportConfig` | `ExportConfig` | No | - | Configuration for export functionality |
| `config` | `DataTableConfig` | No | - | Table configuration options |
| `className` | `string` | No | - | Additional CSS class for the table container |
| `tableClassName` | `string` | No | - | Additional CSS class for the table element |### DataTableConfig Options
| Option | Type | Default | Description |
| -------------------------- | --------- | ------- | ---------------------------------- |
| `enableRowSelection` | `boolean` | `false` | Enable row selection |
| `enableClickRowSelect` | `boolean` | `false` | Allow clicking on row to select it |
| `enableKeyboardNavigation` | `boolean` | `true` | Enable keyboard navigation |
| `enableSearch` | `boolean` | `true` | Show search input |
| `enableDateFilter` | `boolean` | `false` | Show date range filter |
| `enableColumnVisibility` | `boolean` | `true` | Allow toggling column visibility |
| `enableUrlState` | `boolean` | `true` | Save table state in URL |
| `columnResizingTableId` | `string` | - | ID for column resizing persistence |### ExportConfig Options
| Option | Type | Description |
| --------------- | ------------------------ | --------------------------------- |
| `columnMapping` | `Record` | Maps column keys to display names |
| `columnWidths` | `{ wch: number }[]` | Column widths for Excel export |
| `headers` | `string[]` | Column keys to include in export |
| `entityName` | `string` | Name for export files |### ToolbarOptions Interface
| Property | Type | Description |
| -------------------- | ------------ | ----------------------------------------- |
| `selectedRows` | `T[]` | Currently selected rows on current page |
| `allSelectedIds` | `number[]` | IDs of all selected rows across all pages |
| `totalSelectedCount` | `number` | Total number of selected rows |
| `resetSelection` | `() => void` | Function to reset selection |---
## Example Implementations
### Basic Example
```tsx
// src/app/(dashboard)/users/page.tsx
import { Suspense } from "react";
import UsersTable from "./users-table";export default function UsersPage() {
return (
}>
Users
Loading...
);
}
``````tsx
// src/app/(dashboard)/users/users-table/index.tsx
"use client";import { DataTable } from "@/components/data-table/data-table";
import { getColumns } from "./components/columns";
import { useExportConfig } from "./utils/config";
import { fetchUsersByIds } from "@/api/user/fetch-users-by-ids";
import { useUsersData } from "./utils/data-fetching";
import { ToolbarOptions } from "./components/toolbar-options";
import { User } from "./schema";export default function UsersTable() {
return (
getColumns={getColumns}
exportConfig={useExportConfig()}
fetchDataFn={useUsersData}
fetchByIdsFn={fetchUsersByIds}
idField="id"
pageSizeOptions={[10, 20, 50, 100]}
renderToolbarContent={({
selectedRows,
allSelectedIds,
totalSelectedCount,
resetSelection,
}) => (
({
id: row.id,
name: row.name,
}))}
allSelectedIds={allSelectedIds}
totalSelectedCount={totalSelectedCount}
resetSelection={resetSelection}
/>
)}
config={{
enableRowSelection: true,
enableSearch: true,
enableDateFilter: true,
enableColumnVisibility: true,
enableUrlState: true,
}}
/>
);
}
```### Complex Example with Custom Filters
```tsx
// src/app/(dashboard)/orders/orders-table/index.tsx
"use client";import * as React from "react";
import { DataTable } from "@/components/data-table/data-table";
import { getColumns } from "./components/columns";
import { useExportConfig } from "./utils/config";
import { fetchOrdersByIds } from "@/api/order/fetch-orders-by-ids";
import { useOrdersData } from "./utils/data-fetching";
import { ToolbarOptions } from "./components/toolbar-options";
import { Order, OrderStatus } from "./schema";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import { X } from "lucide-react";export default function OrdersTable() {
const [statusFilter, setStatusFilter] = React.useState(
"all"
);// Custom function that extends the base query to add the status filter
const fetchOrdersWithStatus = React.useCallback(
(
page: number,
pageSize: number,
search: string,
dateRange: any,
sortBy: string,
sortOrder: string
) => {
return useOrdersData(
page,
pageSize,
search,
dateRange,
sortBy,
sortOrder,
statusFilter === "all" ? undefined : statusFilter
);
},
[statusFilter]
);// Set isQueryHook property to true to match the DataTable expectations
fetchOrdersWithStatus.isQueryHook = true;// Custom filters to render in the toolbar
const renderCustomFilters = () => (
setStatusFilter(value)}
>
All Statuses
Pending
Processing
Completed
Cancelled
{statusFilter !== "all" && (
Status: {statusFilter}
setStatusFilter("all")}
/>
)}
);return (
getColumns={getColumns}
exportConfig={useExportConfig()}
fetchDataFn={fetchOrdersWithStatus}
fetchByIdsFn={fetchOrdersByIds}
idField="id"
pageSizeOptions={[10, 20, 50, 100]}
renderToolbarContent={({
selectedRows,
allSelectedIds,
totalSelectedCount,
resetSelection,
}) => (
{renderCustomFilters()}
({
id: row.id,
reference: row.reference,
}))}
allSelectedIds={allSelectedIds}
totalSelectedCount={totalSelectedCount}
resetSelection={resetSelection}
/>
)}
config={{
enableRowSelection: true,
enableSearch: true,
enableDateFilter: true,
enableColumnVisibility: true,
enableUrlState: true,
}}
/>
);
}
```By following this documentation and the provided examples, you should now have a complete understanding of how to implement and customize the data table component for your specific needs. The component is designed to be highly flexible while maintaining performance and accessibility.