Skip to content

Repository files navigation

DataTable

A feature-rich, server-side data table implementation built with React, TanStack Table, Laravel, and Inertia.js. This data table provides pagination, sorting, search, bulk actions, and column visibility controls out of the box.

📸 Demo

DataTable OverviewMain datatable view with pagination, search, sorting, column visibility, bulk actions

📱 More Screenshots

Search and FilteringCreate user page (Part of CRUD)

Bulk ActionsEdit user page (Part of CRUD)

🛠️ Tech Stack

LaravelPHPInertiaReactTypeScriptTailwindCSSViteLicense: MIT

Frontend Stack

  • React 18 - Modern UI library with hooks and concurrent features
  • TypeScript - Type-safe JavaScript with excellent developer experience
  • Inertia.js - Modern monolith approach connecting Laravel and React seamlessly
  • TanStack Table - Powerful headless table library for complex data interactions
  • Tailwind CSS - Utility-first CSS framework for rapid UI development
  • Radix UI - Unstyled, accessible UI primitives for custom design systems
  • Lucide Icons - Beautiful & consistent icon library
  • Vite - Fast build tool and development server

Backend Stack

  • Laravel 11 - Elegant PHP framework with rich ecosystem
  • PHP 8.3+ - Modern PHP with performance improvements and type safety
  • MySQL/PostgreSQL - Robust database with full-text search capabilities
  • Laravel Resources - API resource transformation for consistent data formatting
  • Laravel Pagination - Built-in pagination with query string persistence

🚀 Getting Started

Prerequisites

Make sure you have the following installed on your system:

  • PHP 8.2+ with extensions: mbstring, xml, ctype, json, bcmath, fileinfo, tokenizer
  • Composer - PHP dependency manager
  • Node.js 18+ and npm (or yarn/pnpm)
  • MySQL 8.0+ or PostgreSQL 13+
  • Git

Installation

  1. Clone the repository

    git clone https://github.com/your-username/data-table.git
    cd data-table
  2. Install PHP dependencies

    composer install
  3. Install Node.js dependencies

    npm install
    # or
    yarn install
    # or
    pnpm install
  4. Environment setup

    # Copy environment file
    cp .env.example .env
    # Generate application key
    php artisan key:generate
  5. Configure your .env file

    APP_NAME="Data Table"APP_URL=http://localhost:8000DB_CONNECTION=mysqlDB_HOST=127.0.0.1DB_PORT=3306DB_DATABASE=data_tableDB_USERNAME=your_usernameDB_PASSWORD=your_password
  6. Database setup

    # Create database (make sure MySQL/PostgreSQL is running)# Then run migrations
    php artisan migrate
    # Seed with sample data (optional)
    php artisan db:seed
  7. Build frontend assets

    # For development
    npm run dev
    # For production
    npm run build
  8. Start the development server

    # In one terminal - Laravel server
    php artisan serve
    # In another terminal - Vite dev server (for hot reload)
    npm run dev
  9. Access the application

    Open your browser and visit: http://localhost:8000

Quick Development Commands

# Watch for file changes (auto-reload)
npm run dev
# Run Laravel with specific host/port
php artisan serve --host=0.0.0.0 --port=8080
# Clear application cache
php artisan cache:clear
php artisan config:clear
php artisan view:clear
# Run database migrations
php artisan migrate:fresh --seed
# Generate TypeScript types for Laravel routes (if using Ziggy)
php artisan ziggy:generate

Docker Setup (Alternative)

If you prefer using Docker:

# Using Laravel Sail
./vendor/bin/sail up -d
# Install dependencies inside container
./vendor/bin/sail composer install
./vendor/bin/sail npm install
# Run migrations
./vendor/bin/sail artisan migrate --seed
# Build assets
./vendor/bin/sail npm run dev

Troubleshooting

Common Issues:

  1. Vite connection refused: Make sure both php artisan serve and npm run dev are running
  2. Database connection error: Verify database credentials in .env
  3. Permission errors: Set proper permissions:
    chmod -R 775 storage bootstrap/cache
  4. Missing APP_KEY: Run php artisan key:generate

✨ Features

  • 🔍 Server-side Search - Debounced search with query parameter persistence
  • 📄 Server-side Pagination - Configurable page sizes with navigation controls
  • 🔄 Server-side Sorting - Click-to-sort columns with visual indicators
  • Bulk Actions - Select multiple rows and perform batch operations
  • 🗑️ Bulk Delete - Built-in bulk delete functionality with confirmation dialog
  • 👁️ Column Visibility - Show/hide columns with localStorage persistence
  • 📱 Responsive Design - Works on desktop and mobile devices
  • 🎯 TypeScript Support - Fully typed with generic interfaces
  • 🎨 Customizable - Extensible styling and behavior

📋 Table of Contents

🧩 Components Overview

Core Components

ComponentFileDescription
DataTableresources/js/components/datatable.tsxMain table component with all features
DataTableToolbarresources/js/components/datatable-toolbar.tsxSearch, bulk actions, column visibility
DataTablePaginationresources/js/components/datatable-pagination.tsxPagination controls and page size selector
DataTableColumnHeaderresources/js/components/datatable-column-header.tsxSortable column headers with sort indicators

Supporting Files

FileDescription
resources/js/hooks/use-column-visibility.tsxHook for managing column visibility with localStorage
resources/js/types/index.d.tsTypeScript interfaces and types

🚀 Quick Start

1. Basic Implementation

import{DataTable}from'@/components/datatable';import{ColumnDef}from'@tanstack/react-table';// Define your data typeinterfaceUser{id: number;name: string;email: string;created_at: string;}// Define columnsconstcolumns: (ColumnDef<User>&{enable_sorting?: boolean})[]=[{accessorKey: 'id',header: 'ID',enable_sorting: true,},{accessorKey: 'name',header: 'Name',enable_sorting: true,},{accessorKey: 'email',header: 'Email',enable_sorting: true,},];// Use in your page componentfunctionUsersPage({ usersData }: {usersData: PaginatedData<User>}){return<DataTablecolumns={columns}data={usersData.data}paginatedData={usersData}tableKey="users-table"/>;}

2. With Bulk Actions

<DataTablecolumns={columns}data={usersData.data}paginatedData={usersData}activeBulkActions={true}bulkDelete={{route: route('users.bulk-delete'),title: 'Delete Users',description: 'Are you sure you want to delete the selected users?',}}tableKey="users-table"/>

💻 Frontend Usage

Column Definition

Columns follow TanStack Table's ColumnDef interface with an additional enable_sorting property:

constcolumns: (ColumnDef<YourDataType>&{enable_sorting?: boolean})[]=[{accessorKey: 'field_name',header: 'Display Name',enable_sorting: true,// Enable server-side sorting for this columncell: ({ row })=>{// Custom cell renderingreturn<div>{row.original.field_name}</div>;},},{header: 'Actions',accessorKey: 'actions',enable_sorting: false,cell: ({ row })=>{return(<divclassName="flex gap-2"><ButtononClick={()=>editItem(row.original.id)}>Edit</Button><ButtononClick={()=>deleteItem(row.original.id)}>Delete</Button></div>);},},];

Custom Bulk Actions

constbulkActions: BulkAction<User>[]=[{label: 'Export Selected',icon: Download,onClick: (selectedRows)=>{// Handle exportexportUsers(selectedRows);},},{label: 'Archive Selected',icon: Archive,className: 'text-orange-600',onClick: (selectedRows)=>{// Handle archivearchiveUsers(selectedRows);},},];<DataTable// ... other propsbulkActions={bulkActions}activeBulkActions={true}/>;

DataTable Props

interfaceDataTableProps<TData,TValue>{columns: (ColumnDef<TData,TValue>&{enable_sorting?: boolean})[];data: TData[];paginatedData?: PaginatedData<TData>;bulkActions?: BulkAction<TData>[];bulkDelete?: {route: string;title?: string;description?: string;};activeBulkActions?: boolean;tableKey?: string;// For localStorage column visibility}

🛠️ Backend Implementation

1. Controller Method

<?phpnamespaceApp\Http\Controllers;
useApp\Http\Resources\UserResource;
useApp\Models\User;
useIlluminate\Http\Request;
useInertia\Inertia;
class UserController extends Controller
{
publicfunctionindex(Request$request)
{
// Extract query parameters with defaults$queryParams = request()->only(['search', 'page', 'per_page', 'sort_by', 'sort_dir']) + [
'sort_by' => 'id',
'sort_dir' => 'desc',
'per_page' => 10,
'page' => 1
];
$users = User::query()
// Search functionality
->when($request->search, function ($query, $search) {
$query->where('name', 'like', '%' . $search . '%')
->orWhere('email', 'like', '%' . $search . '%');
})
// Sorting
->orderBy($queryParams['sort_by'], $queryParams['sort_dir'])
// Pagination
->paginate($queryParams['per_page'])
->withQueryString();
return Inertia::render('users/index', [
'usersData' => UserResource::collection($users)->additional([
'queryParams' => $queryParams,
]),
]);
}
// Bulk delete methodpublicfunctionbulkDelete(Request$request)
{
$request->validate([
'ids' => 'required|array',
'ids.*' => 'exists:users,id',
]);
User::whereIn('id', $request->ids)->delete();
returnredirect()->route('users.index')
->with('success', 'Users deleted successfully');
}
}

2. Resource Collection

<?phpnamespaceApp\Http\Resources;
useIlluminate\Http\Request;
useIlluminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
publicfunctiontoArray(Request$request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'role' => $this->role,
'created_at' => $this->created_at->format('M d, Y'),
'updated_at' => $this->updated_at->format('M d, Y'),
// Add any other fields you need
];
}
}

3. Routes

// routes/web.php
Route::delete('users/bulk-delete', [UserController::class, 'bulkDelete'])->name('users.bulk-delete');
Route::resource('users', UserController::class);

📚 API Reference

TypeScript Interfaces

// Main data structure returned from backendinterfacePaginatedData<T>{data: T[];queryParams: QueryParams;meta: PaginationMeta;links: SimplePaginationLinks;}// Query parameters for server requestsinterfaceQueryParams{search?: string;page?: number;per_page?: number;sort_by?: string|null;sort_dir?: 'asc'|'desc'|null;[key: string]: unknown;}// Bulk action definitioninterfaceBulkAction<TData>{label: string;icon?: LucideIcon|IconType|null;onClick: (selectedRows: TData[])=>void;className?: string;// For custom styling}// Pagination metadata from LaravelinterfacePaginationMeta{current_page: number;from: number;last_page: number;per_page: number;to: number;total: number;links: Array<{url: string|null;label: string;active: boolean;}>;}

🎯 Examples

Complete Users Table Example

// resources/js/pages/users/index.tsximport{DataTable}from'@/components/datatable';import{Avatar,AvatarFallback,AvatarImage}from'@/components/ui/avatar';import{Badge}from'@/components/ui/badge';import{Button}from'@/components/ui/button';import{AlertDialog,AlertDialogAction,AlertDialogCancel,AlertDialogContent,AlertDialogDescription,AlertDialogFooter,AlertDialogHeader,AlertDialogTitle,AlertDialogTrigger,}from'@/components/ui/alert-dialog';import{ColumnDef}from'@tanstack/react-table';import{Eye,Pencil,Trash}from'lucide-react';constROLE_COLORS={admin: 'border-blue-500 text-blue-500',manager: 'border-green-500 text-green-500',user: 'border-gray-500 text-gray-500',};constUsersIndex=({ usersData }: {usersData: PaginatedData<User>})=>{consthandleDeleteUser=(userId: number)=>{router.delete(route('users.destroy',userId));};constcolumns: (ColumnDef<User>&{enable_sorting?: boolean})[]=[{accessorKey: 'id',header: '#ID',enable_sorting: true,cell: ({ row })=><div>#{row.original.id}</div>,},{header: 'Avatar',accessorKey: 'avatar',enable_sorting: false,cell: ({ row })=>(<AvatarclassName="size-10"><AvatarImagesrc={row.original.avatar}/><AvatarFallback>{row.original.name.charAt(0)}</AvatarFallback></Avatar>),},{accessorKey: 'name',header: 'Name',enable_sorting: true,cell: ({ row })=>(<div><h2className="text-base font-semibold">{row.original.name}</h2><pclassName="text-sm text-gray-500">{row.original.email}</p></div>),},{accessorKey: 'role',header: 'Role',enable_sorting: true,cell: ({ row })=>(<Badgevariant="outline"className={`capitalize ${ROLE_COLORS[row.original.roleaskeyoftypeofROLE_COLORS]}`}>{row.original.role}</Badge>),},{accessorKey: 'created_at',header: 'Created At',enable_sorting: true,},{header: 'Actions',accessorKey: 'actions',enable_sorting: false,cell: ({ row })=>(<divclassName="flex flex-row gap-0.5"><Buttonvariant="ghost"size="icon"className="size-8 text-blue-500"asChild><Linkhref={route('users.show',row.original.id)}><EyeclassName="size-4"/></Link></Button><Buttonvariant="ghost"size="icon"className="size-8 text-green-500"asChild><Linkhref={route('users.edit',row.original.id)}><PencilclassName="size-4"/></Link></Button><AlertDialog><AlertDialogTriggerasChild><Buttonvariant="ghost"size="icon"className="size-8 text-red-500"><TrashclassName="size-4"/></Button></AlertDialogTrigger><AlertDialogContent><AlertDialogHeader><AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle><AlertDialogDescription>
This action cannot be undone. This will permanently delete the user "{row.original.name}".
</AlertDialogDescription></AlertDialogHeader><AlertDialogFooter><AlertDialogCancel>Cancel</AlertDialogCancel><AlertDialogActiononClick={()=>handleDeleteUser(row.original.id)}className="bg-red-600 hover:bg-red-700">
Delete
</AlertDialogAction></AlertDialogFooter></AlertDialogContent></AlertDialog></div>),},];return(<DataTablecolumns={columns}data={usersData.data}paginatedData={usersData}activeBulkActions={true}bulkDelete={{route: route('users.bulk-delete'),title: 'Delete Users',description: 'Are you sure you want to delete the selected users? This action cannot be undone.',}}tableKey="users-table"/>);};

🎨 Customization

Styling

The datatable uses Tailwind CSS classes and follows your existing design system. Key classes can be customized:

  • Table container: .rounded-md.border
  • Selected rows: data-state="selected"
  • Toolbar: .mb-3
  • Pagination: .mt-4

Search Behavior

The search is debounced by 500ms and triggers when:

  • Input length > 2 characters
  • Input is cleared (length = 0)

To customize the debounce timing, modify the useDebouncedCallback in datatable-toolbar.tsx:

consthandleDebouncedSearch=useDebouncedCallback((value: string)=>{// Search logic},300);// Change from 500ms to 300ms

Column Visibility Persistence

Column visibility is automatically saved to localStorage using the tableKey prop. Each table should have a unique key:

<DataTabletableKey="users-table"// Unique identifier// ... other props/>

Pagination Options

Default page size options are defined in datatable-pagination.tsx:

constPER_PAGE_OPTIONS=[10,15,20,25,30,40,50,100];

🔧 Advanced Usage

Custom Search Logic

Extend the backend search to include more fields:

->when($request->search, function ($query, $search) {
$query->where(function ($q) use ($search) {
$q->where('name', 'like', '%' . $search . '%')
->orWhere('email', 'like', '%' . $search . '%')
->orWhere('phone', 'like', '%' . $search . '%')
->orWhereHas('profile', function ($profile) use ($search) {
$profile->where('bio', 'like', '%' . $search . '%');
});
});
})

Advanced Sorting

Handle relationship sorting:

$allowedSorts = ['id', 'name', 'email', 'created_at', 'profile.company'];
if (in_array($queryParams['sort_by'], $allowedSorts)) {
if (str_contains($queryParams['sort_by'], '.')) {
// Handle relationship sorting
[$relation, $field] = explode('.', $queryParams['sort_by']);
$users->join($relation, 'users.id', '=', "{$relation}.user_id")
->orderBy("{$relation}.{$field}", $queryParams['sort_dir']);
} else {
$users->orderBy($queryParams['sort_by'], $queryParams['sort_dir']);
}
}

Error Handling

Add error handling for failed requests:

// In your page componentconst[loading,setLoading]=useState(false);const[error,setError]=useState<string|null>(null);// Wrap router calls with error handlingconsthandleBulkAction=async(selectedRows: User[])=>{try{setLoading(true);setError(null);awaitrouter.delete(route('users.bulk-delete'),{data: {ids: selectedRows.map((row)=>row.id)},onError: (errors)=>{setError('Failed to delete users. Please try again.');},});}catch(err){setError('An unexpected error occurred.');}finally{setLoading(false);}};

🚀 Performance Tips

  1. Use Resource Collections: Always use Laravel Resource Collections to control exactly what data is sent to the frontend
  2. Limit Searchable Fields: Only search fields that are indexed in your database
  3. Optimize Queries: Use select() to limit returned columns, eager load relationships
  4. Debounced Search: The built-in 500ms debounce prevents excessive API calls
  5. Column Visibility: Hidden columns still receive data - consider conditional inclusion in your Resource

🤝 Contributing

To extend the datatable functionality:

  1. Add new features to the appropriate component
  2. Update TypeScript interfaces in types/index.d.ts
  3. Add backend support if needed
  4. Update this documentation
  5. Test with the Users example

📄 License

This project is licensed under the MIT License. See the LICENSE file for details.

MIT License Summary

Commercial use
Modification
Distribution
Private use

Liability
Warranty


Built with ❤️ using React, TanStack Table, Laravel, and Inertia.js

About

Data table with server pagination, query, bulk actions and sorting.

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages