npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@reactorui/datagrid

v1.2.3

Published

A flexible, high-performance React data grid component with TypeScript support, advanced filtering, pagination, sorting, and customizable theming

Downloads

563

Readme

@reactorui/datagrid

npm npm version license

A high-performance, feature-rich React data grid component with TypeScript support, pagination, and advanced filtering capabilities. Designed as a controlled presentation component for maximum flexibility.

🖼️ Screenshots

Light Mode

DataGrid Light Mode

Dark Mode

DataGrid Dark Mode

✨ Features

  • 🚀 High Performance - Optimized rendering with memoization
  • 🔍 Advanced Filtering - Type-aware filters with multiple operators (string, number, date, boolean)
  • 🔄 Flexible Data Sources - Works with any data fetching strategy (REST, GraphQL, local)
  • 📱 Responsive Design - Mobile-first with touch-friendly interactions
  • 🎨 Fully Customizable Theming - Pass custom theme objects to match your design system
  • 🌙 Dark Mode Ready - Built-in dark mode support with zinc palette option
  • Accessibility First - WCAG compliant with keyboard navigation and ARIA labels
  • 🔧 TypeScript Native - Full type safety and comprehensive IntelliSense support
  • 🎯 Rich Event System - 20+ events covering every user interaction
  • 📊 Granular Loading States - Action-specific loading indicators
  • 📜 Scrollable Layout - Fixed headers with maxHeight and stickyHeader props
  • 📥 Load More Support - Incremental data loading with continuation tokens
  • Zero Dependencies - Only React as peer dependency

📦 Installation

npm install @reactorui/datagrid
# or
yarn add @reactorui/datagrid
# or
pnpm add @reactorui/datagrid

🚀 Basic Usage

import { DataGrid } from '@reactorui/datagrid';

const data = [
  { id: 1, name: 'John Doe', email: '[email protected]', age: 28 },
  { id: 2, name: 'Jane Smith', email: '[email protected]', age: 34 },
];

function App() {
  return <DataGrid data={data} />;
}

🛠 With Custom Columns & Styling

import { DataGrid, Column } from '@reactorui/datagrid';

interface User {
  id: number;
  name: string;
  email: string;
  status: 'active' | 'inactive';
  joinDate: string;
}

const columns: Column<User>[] = [
  {
    key: 'name',
    label: 'Full Name',
    sortable: true,
    render: (value, row) => (
      <div className="flex items-center gap-2">
        <div className="w-8 h-8 bg-blue-500 text-white rounded-full flex items-center justify-center text-sm">
          {value.charAt(0)}
        </div>
        {value}
      </div>
    ),
  },
  { key: 'email', label: 'Email Address', sortable: true },
  {
    key: 'status',
    label: 'Status',
    dataType: 'string',
    render: (status) => (
      <span
        className={`px-2 py-1 text-xs font-medium rounded-full ${
          status === 'active' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
        }`}
      >
        {status}
      </span>
    ),
  },
  {
    key: 'joinDate',
    label: 'Join Date',
    dataType: 'date',
    sortable: true,
  },
];

function App() {
  return (
    <DataGrid
      data={users}
      columns={columns}
      variant="bordered"
      size="lg"
      enableSelection={true}
      onSelectionChange={(selected) => console.log('Selected:', selected)}
    />
  );
}

🎨 Custom Theming

The DataGrid supports full theme customization via the theme prop. You can override any part of the default theme to match your design system.

Theme Interface

interface Theme {
  // Container
  container: string;

  // Table
  table: string;
  header: string;
  headerCell: string;
  row: string;
  cell: string;
  selectedRow: string;

  // Controls
  searchInput: string;
  select: string;
  button: string;
  buttonSecondary: string;
  buttonDanger: string;

  // Text
  text: string;
  textMuted: string;
  textError: string;

  // Pagination
  pagination: string;
  paginationButton: string;
  paginationText: string;

  // States
  loadingSkeleton: string;
  emptyState: string;
  errorState: string;

  // Filter
  filterDropdown: string;
  filterTag: string;
  filterTagRemove: string;
}

Custom Theme Example

import { DataGrid, Theme } from '@reactorui/datagrid';

// Define your custom theme (partial overrides supported)
const myTheme: Partial<Theme> = {
  container: 'bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-700',
  row: 'bg-white dark:bg-zinc-900 hover:bg-zinc-50 dark:hover:bg-zinc-800',
  cell: 'px-4 py-3 text-sm text-gray-900 dark:text-zinc-100 border-b dark:border-zinc-700',
  searchInput:
    'px-3 py-2 border dark:border-zinc-700 rounded-lg dark:bg-zinc-800 dark:text-zinc-100',
  pagination:
    'flex items-center justify-between px-4 py-3 dark:bg-zinc-900 border-t dark:border-zinc-700',
};

function App() {
  return <DataGrid data={data} theme={myTheme} />;
}

Using createZincTheme Helper

For projects using the zinc color palette (like Tailwind's neutral grays), use the built-in helper:

import { DataGrid, createZincTheme } from '@reactorui/datagrid';

// Creates a complete theme with zinc palette for dark mode
const zincTheme = createZincTheme('default'); // or 'striped' or 'bordered'

function App() {
  return <DataGrid data={data} theme={zincTheme} />;
}

Theme + Variant

Custom themes merge with the selected variant:

// Striped variant + custom zinc dark mode
<DataGrid
  data={data}
  variant="striped"
  theme={{
    container: 'bg-white dark:bg-zinc-900 rounded-xl',
    row: 'odd:bg-white dark:odd:bg-zinc-900 even:bg-zinc-50 dark:even:bg-zinc-800/50',
  }}
/>

Integration with Design Systems

Example integrating with a custom ThemeStyles system:

// utils/dataGridTheme.ts
import { Theme } from '@reactorui/datagrid';

export const appDataGridTheme: Partial<Theme> = {
  container:
    'bg-white dark:bg-zinc-900 rounded-lg shadow-sm border border-gray-200 dark:border-zinc-700',
  table: 'w-full bg-white dark:bg-zinc-900',
  header: 'bg-gray-50 dark:bg-zinc-800 border-b border-gray-200 dark:border-zinc-700',
  headerCell: 'px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-zinc-400 uppercase',
  row: 'bg-white dark:bg-zinc-900 hover:bg-gray-50 dark:hover:bg-zinc-800 transition-colors',
  cell: 'px-4 py-3 text-sm text-gray-900 dark:text-zinc-100 border-b dark:border-zinc-700',
  selectedRow: 'bg-blue-50 dark:bg-blue-900/20 hover:bg-blue-100 dark:hover:bg-blue-900/30',
  searchInput:
    'px-3 py-2 border dark:border-zinc-700 rounded-md dark:bg-zinc-800 dark:text-zinc-100',
  select: 'px-2 py-1 border dark:border-zinc-700 rounded dark:bg-zinc-800 dark:text-zinc-100',
  button: 'px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700',
  buttonSecondary:
    'px-3 py-2 bg-gray-100 dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 rounded-md',
  text: 'text-gray-700 dark:text-zinc-300',
  textMuted: 'text-gray-500 dark:text-zinc-500',
  textError: 'text-red-600 dark:text-red-400',
  pagination:
    'flex items-center justify-between px-4 py-3 dark:bg-zinc-900 border-t dark:border-zinc-700',
  paginationButton: 'px-3 py-1 text-sm border dark:border-zinc-700 rounded dark:bg-zinc-800',
  paginationText: 'text-sm text-gray-700 dark:text-zinc-300',
  emptyState: 'text-gray-500 dark:text-zinc-400',
  filterDropdown:
    'absolute z-50 mt-2 bg-white dark:bg-zinc-800 border dark:border-zinc-700 rounded-lg shadow-lg',
  filterTag:
    'inline-flex items-center gap-1 px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded-md',
};

// Usage
import { appDataGridTheme } from './utils/dataGridTheme';

<DataGrid data={data} theme={appDataGridTheme} />;

🌐 Server-Side Data (Controlled Mode)

The DataGrid is a controlled presentation component. You handle data fetching; the grid handles display.

import { useState, useEffect } from 'react';
import { DataGrid, LoadingState, ActiveFilter, SortConfig } from '@reactorui/datagrid';

function ServerSideExample() {
  const [data, setData] = useState([]);
  const [loadingState, setLoadingState] = useState<LoadingState>({});
  const [totalRecords, setTotalRecords] = useState(0);
  const [currentPage, setCurrentPage] = useState(1);
  const [pageSize, setPageSize] = useState(25);
  const [error, setError] = useState<string | null>(null);

  // Fetch data from your API
  const fetchData = async (page: number, size: number, filters: ActiveFilter[], search: string) => {
    setLoadingState({ data: true });
    setError(null);

    try {
      const response = await fetch('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ page, pageSize: size, filters, search }),
      });

      const result = await response.json();
      setData(result.items);
      setTotalRecords(result.totalRecords);
    } catch (err) {
      setError('Failed to load data');
    } finally {
      setLoadingState({});
    }
  };

  useEffect(() => {
    fetchData(1, pageSize, [], '');
  }, []);

  return (
    <DataGrid
      data={data}
      loadingState={loadingState}
      totalRecords={totalRecords}
      currentPage={currentPage}
      error={error}
      pageSize={pageSize}
      paginationMode="server"
      filterMode="server"
      // Pagination callbacks - YOU handle the server call
      onPageChange={(page) => {
        setCurrentPage(page);
        fetchData(page, pageSize, [], '');
      }}
      onPageSizeChange={(size) => {
        setPageSize(size);
        setCurrentPage(1);
        fetchData(1, size, [], '');
      }}
      // Filter callbacks - YOU handle the server call
      onApplyFilter={(filter, allFilters) => {
        setLoadingState({ filter: true });
        fetchData(1, pageSize, allFilters, '');
      }}
      onClearFilters={() => {
        fetchData(1, pageSize, [], '');
      }}
      onSearchChange={(term) => {
        setLoadingState({ search: true });
        fetchData(1, pageSize, [], term);
      }}
      onTableRefresh={() => {
        setLoadingState({ refresh: true });
        fetchData(currentPage, pageSize, [], '');
      }}
    />
  );
}

📊 Granular Loading States

Instead of a single loading boolean, use loadingState for action-specific feedback:

interface LoadingState {
  data?: boolean;     // Shows skeleton, disables all controls
  filter?: boolean;   // Spinner on "Apply Filter" button only
  search?: boolean;   // Spinner in search input only
  refresh?: boolean;  // Spinner on refresh button only
  delete?: boolean;   // Spinner on delete button only
}

// Usage
<DataGrid
  data={data}
  loadingState={{ filter: true }}  // Only Apply Filter shows spinner
/>

// Backward compatible - still works
<DataGrid data={data} loading={true} />

Visual Result:

  • Click "Apply Filter" → only that button shows [⟳ Applying...]
  • Click "Refresh" → only that button spins
  • Click "Delete" → only that button shows [⟳ Deleting...]
  • Initial load → table skeleton, all controls disabled

📜 Scrollable Layout

For large datasets, enable scrollable body with fixed headers:

// Fixed pixel height
<DataGrid data={data} maxHeight="400px" />

// Viewport-relative height
<DataGrid data={data} maxHeight="60vh" />

// Dynamic height
<DataGrid data={data} maxHeight="calc(100vh - 200px)" />

// Just sticky headers (browser determines scroll)
<DataGrid data={data} stickyHeader={true} />

// Numeric value (converted to pixels)
<DataGrid data={data} maxHeight={500} />

📥 Load More (Incremental Loading)

For large datasets where you don't want to fetch everything upfront, use the Load More pattern. The button appears in the toolbar when more data is available.

import { useState } from 'react';
import { DataGrid } from '@reactorui/datagrid';

function IncrementalLoadExample() {
  const [data, setData] = useState([]);
  const [continuationToken, setContinuationToken] = useState<string | null>(null);
  const [loadingMore, setLoadingMore] = useState(false);

  const handleLoadMore = async () => {
    setLoadingMore(true);
    try {
      const result = await fetchData({ continuationToken });
      setData((prev) => [...prev, ...result.items]); // Append to existing
      setContinuationToken(result.continuationToken); // null = no more data
    } finally {
      setLoadingMore(false);
    }
  };

  return (
    <DataGrid
      data={data}
      enableLoadMore={true}
      hasMore={continuationToken !== null}
      loadingMore={loadingMore}
      onLoadMore={handleLoadMore}
      pageSize={25} // Pagination still works on loaded data
    />
  );
}

Key Points:

  • Button location: Appears in the toolbar (left of search) when enableLoadMore && hasMore
  • Pagination unaffected: Client-side pagination works normally on loaded data
  • Parent manages state: You control data array, continuation token, and loading state
  • No total required: Works with APIs that only return a continuation token

🎯 Event System

Filter Events

<DataGrid
  data={data}
  enableFilters={true}
  filterMode="server"
  // Called when "Apply Filter" is clicked
  onApplyFilter={(filter, allFilters) => {
    console.log('New filter:', filter);
    console.log('All active filters:', allFilters);
    // Make your API call here
  }}
  // Called when a filter tag X is clicked
  onRemoveFilter={(removedFilter, remainingFilters) => {
    console.log('Removed:', removedFilter);
    // Refetch with remaining filters
  }}
  // Called when "Clear All" is clicked
  onClearFilters={() => {
    console.log('All filters cleared');
    // Refetch without filters
  }}
  // Called on any filter change (convenience callback)
  onFilterChange={(filters) => {
    console.log('Filters changed:', filters.length);
  }}
/>

Pagination & Sort Events

<DataGrid
  data={data}
  onPageChange={(page, paginationInfo) => {
    console.log(`Page ${page} of ${paginationInfo.totalPages}`);
  }}
  onPageSizeChange={(pageSize) => {
    console.log(`Now showing ${pageSize} per page`);
  }}
  onSortChange={(sortConfig) => {
    console.log(`Sorted by ${sortConfig.column} ${sortConfig.direction}`);
  }}
  onSearchChange={(searchTerm) => {
    console.log(`Searching: "${searchTerm}"`);
  }}
  onTableRefresh={() => {
    console.log('Refresh clicked');
  }}
/>

Row & Cell Events

<DataGrid
  data={data}
  onTableRowClick={(row, event) => {
    console.log('Clicked:', row);
  }}
  onTableRowDoubleClick={(row, event) => {
    openEditModal(row);
    return false; // Prevent selection toggle
  }}
  onTableRowHover={(row, event) => {
    row ? showTooltip(row) : hideTooltip();
  }}
  onRowSelect={(row, isSelected) => {
    console.log(`${row.name} ${isSelected ? 'selected' : 'deselected'}`);
  }}
  onSelectionChange={(selectedRows) => {
    setBulkActionsEnabled(selectedRows.length > 0);
  }}
  onCellClick={(value, row, column, event) => {
    if (column.key === 'email') {
      window.open(`mailto:${value}`);
    }
  }}
/>

🗑️ Delete Functionality

<DataGrid
  data={data}
  enableSelection={true}
  enableDelete={true}
  deleteConfirmation={true} // Shows confirm dialog
  loadingState={{ delete: isDeleting }}
  onBulkDelete={async (selectedRows) => {
    setLoadingState({ delete: true });
    await deleteUsers(selectedRows.map((r) => r.id));
    setLoadingState({});
    refetchData();
  }}
/>

📋 Props Reference

Data & State

| Prop | Type | Default | Description | | -------------- | ---------------- | ------------- | -------------------------------------- | | data | T[] | Required | Array of data to display | | columns | Column<T>[] | Auto-detected | Column definitions | | loading | boolean | false | Simple loading state (backward compat) | | loadingState | LoadingState | {} | Granular loading states | | totalRecords | number | - | Total records for pagination display | | currentPage | number | - | Controlled current page | | error | string \| null | - | Error message to display |

Layout & Styling

| Prop | Type | Default | Description | | -------------- | -------------------------------------- | ----------- | ------------------------------------ | | maxHeight | string \| number | - | Fixed height with scrollable body | | stickyHeader | boolean | false | Enable sticky table header | | className | string | '' | Additional CSS classes | | variant | 'default' \| 'striped' \| 'bordered' | 'default' | Visual theme variant | | size | 'sm' \| 'md' \| 'lg' | 'md' | Padding/text size | | theme | Partial<Theme> | - | Custom theme overrides (see Theming) |

Features

| Prop | Type | Default | Description | | -------------------- | ----------------------------------------- | ---------- | --------------------- | | enableSearch | boolean | true | Show search input | | enableSorting | boolean | true | Enable column sorting | | enableFilters | boolean | true | Show filter controls | | enableSelection | boolean | true | Show row checkboxes | | enableDelete | boolean | false | Show delete button | | enableRefresh | boolean | false | Show refresh button | | deleteConfirmation | boolean | false | Confirm before delete | | paginationMode | 'client' \| 'server' | 'client' | Pagination behavior | | filterMode | 'client' \| 'server' \| 'client&server' | 'client' | Filter behavior |

Load More

| Prop | Type | Default | Description | | ---------------- | ------------ | ------- | --------------------------------------- | | enableLoadMore | boolean | false | Show "Load More" button in toolbar | | hasMore | boolean | false | Whether more data is available to load | | loadingMore | boolean | false | Show loading spinner on button | | onLoadMore | () => void | - | Called when Load More button is clicked |

paginationMode options:

  • 'client' (default) - Slices data locally, totalRecords is display-only
  • 'server' - No local slicing, parent handles pagination via onPageChange/onPageSizeChange

filterMode options:

  • 'client' - Filters locally, no callbacks fired
  • 'server' - Fires callbacks only, no local filtering
  • 'client&server' - Filters locally AND fires callbacks

Pagination

| Prop | Type | Default | Description | | ----------------- | ---------- | ------------------ | -------------------------- | | pageSize | number | 10 | Items per page | | pageSizeOptions | number[] | [5,10,25,50,100] | Page size dropdown options |

Event Callbacks

| Event | Signature | Description | | ----------------------- | ------------------------------------- | -------------------- | | onApplyFilter* | (filter, allFilters) => void | Filter applied | | onRemoveFilter* | (removed, remaining) => void | Filter tag removed | | onClearFilters* | () => void | Clear All clicked | | onFilterChange* | (filters) => void | Any filter change | | onSearchChange | (term) => void | Search input changed | | onSortChange | (sortConfig) => void | Column sort changed | | onPageChange | (page, info) => void | Page navigation | | onPageSizeChange | (size) => void | Page size changed | | onTableRefresh | () => void | Refresh clicked | | onTableRowClick | (row, event) => void | Row clicked | | onTableRowDoubleClick | (row, event) => boolean \| void | Row double-clicked | | onTableRowHover | (row \| null, event) => void | Row hover | | onRowSelect | (row, isSelected) => void | Single row selection | | onSelectionChange | (rows) => void | Selection changed | | onCellClick | (value, row, column, event) => void | Cell clicked | | onBulkDelete | (rows) => void | Delete clicked | | onLoadMore | () => void | Load More clicked |

* Filter callbacks only fire when filterMode="server" or filterMode="client&server"

Column Configuration

interface Column<T> {
  key: keyof T | string;
  label: string;
  sortable?: boolean; // Default: true
  filterable?: boolean; // Default: true
  dataType?: 'string' | 'number' | 'boolean' | 'date' | 'datetime';
  width?: string | number;
  minWidth?: string | number;
  maxWidth?: string | number;
  align?: 'left' | 'center' | 'right';
  render?: (value: any, row: T, index: number) => ReactNode;
}

🎨 Theming & Styling

Built-in Variants

// Clean, minimal design
<DataGrid variant="default" data={data} />

// Alternating row colors
<DataGrid variant="striped" data={data} />

// Full borders around cells
<DataGrid variant="bordered" data={data} />

// Size variants
<DataGrid size="sm" data={data} />  // Compact
<DataGrid size="md" data={data} />  // Standard
<DataGrid size="lg" data={data} />  // Comfortable

// Dark mode (automatic with Tailwind)
<div className="dark">
  <DataGrid data={data} />
</div>

Custom Theme

import { DataGrid, createZincTheme } from '@reactorui/datagrid';

// Option 1: Use zinc theme helper
<DataGrid data={data} theme={createZincTheme('default')} />

// Option 2: Partial overrides
<DataGrid
  data={data}
  theme={{
    container: 'bg-white dark:bg-slate-900 rounded-2xl',
    row: 'hover:bg-slate-50 dark:hover:bg-slate-800',
  }}
/>

// Option 3: Complete custom theme
<DataGrid data={data} theme={myCompleteTheme} />

🧪 Testing

npm test              # Run tests
npm run test:watch    # Watch mode
npm run test:coverage # Coverage report
import { render, screen, fireEvent } from '@testing-library/react';
import { DataGrid } from '@reactorui/datagrid';

test('handles filter application', async () => {
  const onApplyFilter = jest.fn();
  render(
    <DataGrid
      data={testData}
      enableFilters={true}
      filterMode="server"
      onApplyFilter={onApplyFilter}
    />
  );

  // Select column, enter value, click Apply
  const selects = screen.getAllByRole('combobox');
  fireEvent.change(selects[0], { target: { value: 'name' } });

  const input = screen.getByPlaceholderText('Enter value');
  fireEvent.change(input, { target: { value: 'John' } });

  fireEvent.click(screen.getByRole('button', { name: /apply filter/i }));

  expect(onApplyFilter).toHaveBeenCalledWith(
    expect.objectContaining({ column: 'name', value: 'John' }),
    expect.any(Array)
  );
});

test('applies custom theme', () => {
  const customTheme = { container: 'custom-class dark:bg-zinc-900' };
  const { container } = render(<DataGrid data={testData} theme={customTheme} />);

  expect(container.firstChild).toHaveClass('dark:bg-zinc-900');
});

test('client mode slices data locally', () => {
  const largeData = Array.from({ length: 50 }, (_, i) => ({ id: i, name: `User ${i}` }));
  render(<DataGrid data={largeData} pageSize={10} paginationMode="client" />);

  // Should only show 10 rows
  const rows = screen.getAllByRole('row');
  expect(rows.length - 1).toBe(10); // minus header
});

test('server mode does not slice data', () => {
  const pageData = Array.from({ length: 5 }, (_, i) => ({ id: i, name: `User ${i}` }));
  render(<DataGrid data={pageData} totalRecords={100} pageSize={10} paginationMode="server" />);

  // Shows all 5 rows (server already sliced)
  const rows = screen.getAllByRole('row');
  expect(rows.length - 1).toBe(5);

  // But displays totalRecords
  expect(screen.getByText(/of 100 records/)).toBeInTheDocument();
});

test('shows Load More button when enabled and hasMore', () => {
  const onLoadMore = jest.fn();
  render(<DataGrid data={testData} enableLoadMore={true} hasMore={true} onLoadMore={onLoadMore} />);

  const loadMoreButton = screen.getByText('Load More');
  expect(loadMoreButton).toBeInTheDocument();

  fireEvent.click(loadMoreButton);
  expect(onLoadMore).toHaveBeenCalledTimes(1);
});

test('hides Load More button when hasMore is false', () => {
  render(<DataGrid data={testData} enableLoadMore={true} hasMore={false} />);

  expect(screen.queryByText('Load More')).not.toBeInTheDocument();
});

⚠️ Migration Guide

Deprecated Props (v1.x → v2.0)

The following props are deprecated and will show console warnings. They will be removed in the next major version:

| Deprecated Prop | Replacement | Notes | | ---------------------- | ------------------- | ----------------------------------- | | endpoint | Use data prop | Fetch data in parent, pass to grid | | httpConfig | Use data prop | Handle auth/headers in parent fetch | | serverPageSize | pageSize | Use standard pageSize prop | | onDataLoad | Use data prop | Handle in parent after fetch | | onDataError | error prop | Pass error message as prop | | onLoadingStateChange | loadingState prop | Use granular loading states |

Before (Old API)

// ❌ Deprecated approach
<DataGrid
  endpoint="/api/users"
  httpConfig={{ bearerToken: 'xxx' }}
  serverPageSize={100}
  onDataLoad={(res) => console.log(res)}
  onDataError={(err) => console.error(err)}
/>

After (New API)

// ✅ Recommended approach
function MyGrid() {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const fetchData = async () => {
    setLoading(true);
    try {
      const res = await fetch('/api/users', {
        headers: { Authorization: 'Bearer xxx' },
      });
      setData(await res.json());
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  return <DataGrid data={data} loadingState={{ data: loading }} error={error} pageSize={25} />;
}

Filter Mode Change

Breaking: Filter callbacks now require filterMode to be set:

// ❌ Won't fire callbacks (filterMode defaults to 'client')
<DataGrid
  data={data}
  onApplyFilter={(f) => console.log(f)}  // Never called!
/>

// ✅ Set filterMode to enable callbacks
<DataGrid
  data={data}
  filterMode="server"  // or "client&server"
  onApplyFilter={(f) => fetchWithFilter(f)}
/>

Pagination Mode (New in v1.2.2)

totalRecords no longer automatically triggers server-side pagination. Use explicit paginationMode:

// Client-side pagination (default) - totalRecords is display only
<DataGrid
  data={allData}
  totalRecords={500}  // Just for display: "Showing 1-25 of 500"
  pageSize={25}
  // paginationMode="client" is the default
/>

// Server-side pagination - parent handles slicing
<DataGrid
  data={currentPageData}
  totalRecords={500}
  paginationMode="server"  // Explicit opt-in
  onPageChange={handlePageChange}
  onPageSizeChange={handlePageSizeChange}
/>

🔧 Development

npm install        # Install dependencies
npm test           # Run tests
npm run build      # Build library
npm run typecheck  # Type checking
npm run lint       # Linting

📤 Publishing

npm run build                    # Build the package
npm version patch|minor|major    # Bump version
npm publish --access public      # Publish to npm

📄 License

MIT License - see LICENSE file for details.

Related Projects

Part of the ReactorUI ecosystem:


Made with ❤️ by ReactorUI