Skip to content

snowpact/react-tanstack-query-table

Repository files navigation

@snowpact/react-tanstack-query-table

Ultra-light, registry-based data table for React + TanStack Table + TanStack Query.

Live Demo

Features

  • Zero heavy dependencies: Only @tanstack/react-query and @tanstack/react-table as peer dependencies
  • Registry-based: Inject your own i18n and Link component
  • TypeScript: Full type support with generics
  • Two modes: Client-side and Server-side pagination/filtering/sorting
  • Customizable: Override styles via CSS variables

Quick Setup

1. Install

npm install @tanstack/react-query @tanstack/react-table
npm install @snowpact/react-tanstack-query-table

2. Import styles

// In your app entry point (main.tsx or App.tsx)
import '@snowpact/react-tanstack-query-table/styles.css';

3. Setup once

// In your app entry point (main.tsx or App.tsx)
import { setupSnowTable } from '@snowpact/react-tanstack-query-table';
import { Link } from 'react-router-dom';
import { t } from './i18n'; // Your translation function

setupSnowTable({
  translate: (key) => t(key),
  LinkComponent: Link,
});

Translation keys:

  • Dynamic keys (column labels, etc.) - Your translate function handles these
  • Static UI keys (dataTable.*) - Built-in English defaults if translate returns the key unchanged
Key Default
dataTable.search "Search..."
dataTable.elements "elements"
dataTable.paginationSize "per page"
dataTable.columnsConfiguration "Columns"
dataTable.resetFilters "Reset filters"
dataTable.resetColumns "Reset"
dataTable.searchFilters "Search..."
dataTable.searchEmpty "No results found"
dataTable.selectFilter "Select..."

Override static keys without i18n:

setupSnowTable({
  translate: (key) => key,
  LinkComponent: Link,
  translations: { 'dataTable.search': 'Rechercher...' },
});

4. Use the table

import { SnowClientDataTable, SnowColumnConfig } from '@snowpact/react-tanstack-query-table';

type User = { id: string; name: string; email: string; status: string };

const columns: SnowColumnConfig<User>[] = [
  { key: 'name', label: 'Name' },
  { key: 'email', label: 'Email' },
  { key: 'status', label: 'Status', render: (item) => <Badge>{item.status}</Badge> },
];

<SnowClientDataTable
  queryKey={['users']}
  fetchAllItemsEndpoint={() => fetchUsers()}
  columnConfig={columns}
  enableGlobalSearch
  enablePagination
  enableSorting
  enableColumnConfiguration
  defaultPageSize={20}
  defaultSortBy="name"
  defaultSortOrder="asc"
  persistState
/>

That's it! You have a working data table.


Advanced Configuration

Theme Customization

Override CSS variables to match your design:

:root {
  --snow-background: #ffffff;
  --snow-foreground: #0a0a0a;
  --snow-secondary: #f5f5f5;
  --snow-secondary-foreground: #737373;
  --snow-border: #d4d4d4;
  --snow-ring: #a3a3a3;
  --snow-radius: 0.375rem;
}

/* Dark mode */
.dark {
  --snow-background: #1a1a2e;
  --snow-foreground: #eaeaea;
  --snow-secondary: #16213e;
  --snow-secondary-foreground: #a0a0a0;
  --snow-border: #0f3460;
  --snow-ring: #3b82f6;
}

Client vs Server Mode

Mode Component Use case Data handling
Client SnowClientDataTable < 5,000 items All data loaded, filtered/sorted locally
Server SnowServerDataTable > 5,000 items Server handles pagination/filtering

SnowClientDataTable

Fetches all data once, handles everything in the browser:

<SnowClientDataTable
  queryKey={['users']}
  fetchAllItemsEndpoint={() => api.getUsers()}
  columnConfig={columns}
/>

SnowServerDataTable

Server handles pagination, search, filtering, and sorting:

import { SnowServerDataTable, ServerFetchParams } from '@snowpact/react-tanstack-query-table';

const fetchUsers = async (params: ServerFetchParams) => {
  // params: { limit, offset, search?, sortBy?, sortOrder?, filters?, prefilter? }
  const response = await api.getUsers(params);
  return {
    items: response.data,
    totalItemCount: response.total,
  };
};

<SnowServerDataTable
  queryKey={['users']}
  fetchServerEndpoint={fetchUsers}
  columnConfig={columns}
/>

Actions

Actions appear as buttons in each row:

Click Action

{
  type: 'click',
  icon: EditIcon,
  label: 'Edit',
  onClick: (item) => openEditModal(item),
}

Link Action

{
  type: 'link',
  icon: EyeIcon,
  label: 'View',
  href: (item) => `/users/${item.id}`,
  external: false,  // true for target="_blank"
}

Endpoint Action

For API calls with built-in mutation handling:

{
  type: 'endpoint',
  icon: TrashIcon,
  label: 'Delete',
  variant: 'danger',
  endpoint: (item) => api.deleteUser(item.id),
  onSuccess: () => {
    toast.success('User deleted');
    queryClient.invalidateQueries(['users']);
  },
  onError: (error) => toast.error(error.message),
}

Endpoint with Confirmation

Use withConfirm to show a confirmation dialog before the endpoint is called:

{
  type: 'endpoint',
  icon: TrashIcon,
  label: 'Delete',
  variant: 'danger',
  endpoint: (item) => api.deleteUser(item.id),
  withConfirm: async (item) => {
    // Return true to proceed, false to cancel
    return window.confirm(`Delete ${item.name}?`);
    // Or use your own dialog library (e.g., sweetalert2, radix-ui/dialog)
  },
  onSuccess: () => queryClient.invalidateQueries(['users']),
}

The endpoint is only called if withConfirm returns true (or a truthy Promise).

Dynamic Actions

actions={[
  (item) => ({
    type: 'click',
    icon: item.isActive ? PauseIcon : PlayIcon,
    label: item.isActive ? 'Deactivate' : 'Activate',
    onClick: () => toggleStatus(item),
    hidden: item.role === 'admin',
  }),
]}

Filters

Global Search

<SnowClientDataTable
  enableGlobalSearch
  texts={{ searchPlaceholder: 'Search users...' }}
/>

Column Filters

<SnowClientDataTable
  filters={[
    {
      key: 'status',
      label: 'Status',
      multipleSelection: true,  // Allow multiple values
      options: [
        { value: 'active', label: 'Active' },
        { value: 'inactive', label: 'Inactive' },
      ],
    },
    {
      key: 'role',
      label: 'Role',
      options: [
        { value: 'admin', label: 'Admin' },
        { value: 'user', label: 'User' },
      ],
    },
  ]}
/>

Prefilters (Tabs)

<SnowClientDataTable
  prefilters={[
    { id: 'all', label: 'All' },
    { id: 'active', label: 'Active' },
  ]}
  prefilterFn={(item, prefilterId) => {
    if (prefilterId === 'all') return true;
    return item.status === prefilterId;
  }}
/>

Other Features

URL State Persistence

<SnowClientDataTable persistState />

Saves pagination, search, filters, and sorting in URL params.

Column Configuration

<SnowClientDataTable
  enableColumnConfiguration
  columnConfig={[
    { key: 'name' },
    { key: 'details', meta: { defaultHidden: true } },
  ]}
/>

Sorting

<SnowClientDataTable
  enableSorting
  defaultSortBy="createdAt"
  defaultSortOrder="desc"
/>

Row Click

<SnowClientDataTable
  onRowClick={(item) => navigate(`/users/${item.id}`)}
  activeRowId={selectedUserId}
/>

Custom Column Rendering

const columns: SnowColumnConfig<User>[] = [
  { key: 'name', label: 'Name' },
  {
    key: 'status',
    label: 'Status',
    render: (item) => (
      <span className={item.status === 'active' ? 'text-green-500' : 'text-red-500'}>
        {item.status}
      </span>
    ),
  },
  {
    key: '_extra_fullName',  // Use _extra_ prefix for computed columns
    label: 'Full Name',
    render: (item) => `${item.firstName} ${item.lastName}`,
    searchableValue: (item) => `${item.firstName} ${item.lastName}`,
  },
];

Column Metadata (meta)

Use meta to customize column appearance and behavior:

import { SnowColumnConfig, SnowColumnMeta } from '@snowpact/react-tanstack-query-table';

const columns: SnowColumnConfig<User>[] = [
  {
    key: 'id',
    label: 'ID',
    meta: {
      width: '80px',
      center: true,
    },
  },
  {
    key: 'name',
    label: 'Name',
    meta: {
      minWidth: '150px',
      maxWidth: '300px',
    },
  },
  {
    key: 'description',
    label: 'Description',
    meta: {
      defaultHidden: true,  // Hidden by default in column configuration
    },
  },
  {
    key: 'actions',
    label: '',
    meta: {
      width: 'auto',
      disableColumnClick: true,  // Don't trigger onRowClick for this column
    },
  },
];

SnowColumnMeta options

Option Type Description
width string | number Column width (e.g., '200px', '20%', 'auto')
minWidth string | number Minimum column width
maxWidth string | number Maximum column width
defaultHidden boolean Hide column by default (with enableColumnConfiguration)
disableColumnClick boolean Disable onRowClick for this column
center boolean Center column content

API Reference

SnowClientDataTable Props

Prop Type Default Description
queryKey string[] Required React Query cache key
fetchAllItemsEndpoint () => Promise<T[]> Required Data fetching function
columnConfig SnowColumnConfig<T>[] Required Column definitions
actions TableAction<T>[] - Row actions
filters FilterConfig<T>[] - Column filters
prefilters PreFilter[] - Tab filters
prefilterFn (item, id) => boolean - Client-side prefilter logic
persistState boolean false Persist state in URL
enableGlobalSearch boolean false Enable search bar
enablePagination boolean true Enable pagination
enableSorting boolean true Enable column sorting
enableColumnConfiguration boolean false Enable column visibility toggle
defaultPageSize number 10 Initial page size
defaultSortBy string - Initial sort column
defaultSortOrder 'asc' | 'desc' 'asc' Initial sort direction

SnowServerDataTable Props

Same as SnowClientDataTable, plus:

Prop Type Description
fetchServerEndpoint (params: ServerFetchParams) => Promise<ServerPaginatedResponse<T>> Paginated fetch function

ServerFetchParams

interface ServerFetchParams {
  limit: number;
  offset: number;
  search?: string;
  prefilter?: string;
  filters?: Record<string, string[]>;
  sortBy?: string;
  sortOrder?: 'ASC' | 'DESC';
}

ServerPaginatedResponse

interface ServerPaginatedResponse<T> {
  items: T[];
  totalItemCount: number;
}

License

MIT

About

Ultra-light, registry-based data table for React, TanStack Table and TanStack Query

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •  

Languages