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

@xinosolutions/react-datatable

v1.2.2

Published

A modern, feature-rich React DataTable component with search, selection, and customizable columns

Readme

@xinosolutions/react-datatable

A modern React DataTable with search, pagination, row selection, mobile card layout, and theming.

npm version License: MIT


About XinoSolutions

XinoSolutions is a software development company dedicated to creating high-quality, developer-friendly solutions. We specialize in building modern React components and tools that help developers build better applications faster.

This package is part of our open-source initiative to contribute valuable tools to the React ecosystem.


Features

  • Real-time Search — Local filter by default; pass search={{ mode: "server", ... }} only when the API owns the query
  • Pagination — Local paging by default; pass pagination={{ mode: "server", ... }} only when the API returns one page at a time
  • Row Selection — Multi-select (checkbox) or single-select (radio)
  • Customizable Columns — Text, number, HTML, action menu, custom render
  • Mobile Layout — Card/stack rows by default under 768px (mobileLayout="table" to keep the grid)
  • Sticky Header — Enabled when maxHeight / height constrains the table body
  • Natural height — Omit both height props for page-level vertical scroll (horizontal scroll only inside the table)
  • Sticky Columns — Selection column stays visible while scrolling horizontally (or the first data column when selection is off)
  • Theme Customization — Brand color via --table-theme-color
  • Empty & Loading States — Clear empty and loading UI
  • TypeScript Types — Bundled index.d.ts
  • Zero Runtime Dependencies — Peer dependency on React only

Installation

npm install @xinosolutions/react-datatable

Quick Start

import React, { useState } from 'react';
import { DataTable } from '@xinosolutions/react-datatable';

function App() {
  const [selected, setSelected] = useState([]);

  const rows = [
    { id: 1, name: 'John Doe', email: '[email protected]', role: 'Developer' },
    { id: 2, name: 'Jane Smith', email: '[email protected]', role: 'Designer' },
    { id: 3, name: 'Bob Johnson', email: '[email protected]', role: 'Manager' },
  ];

  const columns = [
    { key: 'id', label: 'ID' },
    { key: 'name', label: 'Name' },
    { key: 'email', label: 'Email' },
    { key: 'role', label: 'Role' },
  ];

  return (
    <DataTable
      rows={rows}
      columns={columns}
      maxHeight={480}
      checkboxSelection={{
        selected,
        setSelected,
        selectBy: 'id',
      }}
    />
  );
}

export default App;

Props

DataTable Props

| Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | rows | Array<Object> | Yes | [] | Row data. In pagination.mode: "server", pass only the current page of rows (do not pass the full dataset). | | columns | Array<Column> | Yes | [] | Column config | | pagination | Object | No | See below | Client or server pagination. Default is client-side (mode: "client"). | | search | Object | No | See below | Client or server search. Default is client-side (mode: "client"). | | checkboxSelection | Object | No | — | Selection config (checkbox or radio) | | theme | Object | No | — | Theme CSS variables | | handleMenu | (row) => MenuItem[] | No | — | Menu items for type: "action" columns | | title | string \| null | No | "Search Table Data" | Header title (null / "" hides it) | | showSearch | boolean | No | true | Show the search input | | searchPlaceholder | string | No | "Search" | Search placeholder | | showResultCount | boolean | No | true | Client: “filtered of loaded”. Server pagination: totalCount | | maxHeight | string \| number | No | — | Max height of table body (enables sticky header + vertical scroll) | | height | string \| number | No | — | Fixed height of table body (same sticky / vertical-scroll behavior) | | mobileLayout | "cards" \| "table" | No | "cards" | Mobile (≤768px) layout mode | | sanitizeHtml | (html: string) => string | No | — | Sanitizer for type: "html" cells | | loading | boolean | No | false | Show loading state in the table body. In server mode the pager stays visible so the user can change page while a request is in flight. |

Pagination Object

| Property | Type | Default | Description | |----------|------|---------|-------------| | mode | "client" \| "server" | "client" | "client": table slices rows in the browser. "server": rows is already one page; parent fetches and owns totals. | | showTopPagination | boolean | false | Show pagination above the table | | showBottomPagination | boolean | true | Show pagination below the table | | defaultPageSize | number | 50 | Initial page size in client mode. Fallback in server mode if pageSize is omitted. | | pageSizeOptions | Array<number> | [10, 50, 100, 500] | Values in the “per page” select. Include your current pageSize. | | totalCount | number | — | Required in server mode. Total rows across all pages (not rows.length). | | page | number | — | Required in server mode. Current page, 1-based (1 is the first page). | | pageSize | number | — | Required in server mode. Controlled page size. | | onPageChange | (page: number) => void | — | Required in server mode. Called with the next 1-based page. | | onPageSizeChange | (pageSize: number) => void | — | Required in server mode. Called with the next page size. Reset page to 1 here. |

Client mode (mode: "client", default)

Pass the full (or already-filtered) list as rows. The table searches, slices, and keeps page/size internally. defaultPageSize and pageSizeOptions are enough:

<DataTable
  rows={allRows}
  columns={columns}
  pagination={{ defaultPageSize: 25, pageSizeOptions: [10, 25, 50] }}
/>

Server mode (mode: "server")

Use this when the API returns one page at a time (same idea as custom_pagination on the older MUI table).

| You pass | Table does | |----------|------------| | rows = current page only | Renders them as-is (no second slice) | | totalCount | Drives page count and “Showing X to Y of Z” | | page (1-based) + pageSize | Controls the pager and # column ((page - 1) * pageSize + index + 1) | | onPageChange / onPageSizeChange | Forwards clicks; does not keep its own page state |

mode: "server" is opt-in. Omit pagination, or pass only defaultPageSize / pageSizeOptions / show flags, and the table still slices rows locally like before.

This package is 1-based. MUI TablePagination is 0-based. If you migrate from custom_pagination.page, use page + 1 when talking to DataTable, and newPage (already 1-based) when talking to your API if the API is 1-based — or convert explicitly.

Keep page in range when totalCount shrinks. The table does not clamp server page for you.

Typical fetch (pagination + search):

const [page, setPage] = useState(1);       // 1-based
const [pageSize, setPageSize] = useState(50);
const [searchValue, setSearchValue] = useState('');
const [appliedSearch, setAppliedSearch] = useState('');
const [rows, setRows] = useState([]);
const [totalCount, setTotalCount] = useState(0);
const [loading, setLoading] = useState(false);

useEffect(() => {
  let cancelled = false;
  setLoading(true);

  fetchPage({ page, pageSize, search: appliedSearch }).then((result) => {
    if (cancelled) return;
    setRows(result.rows);           // one page
    setTotalCount(result.totalCount);
    setLoading(false);
  });

  return () => {
    cancelled = true;
  };
}, [page, pageSize, appliedSearch]);

<DataTable
  rows={rows}
  columns={columns}
  loading={loading}
  search={{
    mode: 'server',
    value: searchValue,
    onChange: setSearchValue,
    onSubmit: (value) => {
      setPage(1);
      setAppliedSearch(value);
    },
  }}
  pagination={{
    mode: 'server',
    totalCount,
    page,
    pageSize,
    onPageChange: setPage,
    onPageSizeChange: (nextSize) => {
      setPageSize(nextSize);
      setPage(1);
    },
    pageSizeOptions: [10, 50, 100, 500],
  }}
/>

If your API uses 0-based pages, convert at the boundary:

fetchPage({ page: page - 1, limit: pageSize });

Search Object

Same idea as pagination: client by default, mode: "server" when the API owns the query (replaces custom_search on the older MUI table).

| Property | Type | Default | Description | |----------|------|---------|-------------| | mode | "client" \| "server" | "client" | "client": filter rows as the user types. "server": controlled input; does not filter rows. | | value | string | — | Required in server mode. Controlled input value. | | onChange | (value: string) => void | — | Required in server mode. Called on each keystroke (and when Clear is clicked). | | onSubmit | (value: string) => void | — | Optional. Enter key and a Search button. Typical: set page to 1 and refetch. If omitted, fetch from onChange (or a debounce) in the parent. |

showSearch / searchPlaceholder still control visibility and placeholder.

mode: "server" is opt-in. Omit the search prop entirely and search stays local (filters rows as the user types), including when pagination is in server mode.

Client mode (search.mode: "client", default)

Omit search. The table keeps the query internally and filters rows locally.

Server mode (search.mode: "server")

| You pass | Table does | |----------|------------| | value + onChange | Controlled input (type and Clear) | | onSubmit (optional) | Enter + Search button; parent should refetch and reset page to 1 | | Already-filtered rows | No second client-side filter |

Live search (fetch as the user types — debounce in the parent if needed):

search={{
  mode: 'server',
  value: searchValue,
  onChange: (value) => {
    setSearchValue(value);
    setPage(1);
  },
}}

Submit-to-search (same pattern as custom_search.handleSubmit):

search={{
  mode: 'server',
  value: searchValue,
  onChange: setSearchValue,
  onSubmit: (value) => {
    setPage(1);
    setAppliedSearch(value);
  },
}}

CheckboxSelection Object

| Property | Type | Required | Description | |----------|------|----------|-------------| | selected | Array<Object> | Yes | Selected rows (0–1 items when mode: "single") | | setSelected | Function | Yes | State setter for selected rows | | selectBy | string | No | Row id key (default "_id") | | mode | "multiple" \| "single" | No | "multiple" = checkboxes, "single" = radio (default "multiple") |

Theme Object

| Property | Type | Description | |----------|------|-------------| | --table-theme-color | string | Theme accent (default #4FAFA0) |

Column Shape

| Field | Type | Description | |-------|------|-------------| | key | string | Field name on the row | | label | string | Header / card label | | type | "number" \| "html" \| "action" | Special column types | | hideOnMobile | boolean | Hide column under 768px | | render | (row, index) => ReactNode | Custom cell renderer |


Column Types

Standard / Number / Custom Render / Action

const columns = [
  { label: '#', type: 'number' },
  { key: 'name', label: 'Name' },
  { key: 'phone', label: 'Phone', hideOnMobile: true },
  {
    label: 'Status',
    render: (row) => <strong>{row.status}</strong>,
  },
  { label: 'Actions', type: 'action' },
];

const handleMenu = (row) => [
  {
    label: 'Edit',
    icon: <EditIcon />, // any React node (SVG, emoji, icon component)
    onClick: () => edit(row),
  },
  {
    label: 'Delete',
    icon: <TrashIcon />,
    danger: true, // optional destructive styling
    onClick: () => remove(row),
  },
];

<DataTable rows={rows} columns={columns} handleMenu={handleMenu} />

Menu item shape: { label, icon?, danger?, onClick? }.

HTML Column

<DataTable
  rows={rows}
  columns={[{ key: 'bio', label: 'Bio', type: 'html' }]}
  sanitizeHtml={(html) => yourSanitize(html)} // optional
/>

Examples

Natural height (horizontal scroll only)

Omit maxHeight / height so the table grows with its rows. The page scrolls vertically; the header is not sticky; only horizontal overflow stays inside the table.

<DataTable
  rows={rows}
  columns={columns}
  // no maxHeight / height
/>

Constrained height (sticky header + vertical scroll)

<DataTable
  rows={rows}
  columns={columns}
  maxHeight={480}
/>

Single-select (radio)

<DataTable
  rows={rows}
  columns={columns}
  checkboxSelection={{
    selected,
    setSelected,
    selectBy: 'id',
    mode: 'single',
  }}
/>

Force table layout on mobile

<DataTable
  rows={rows}
  columns={columns}
  mobileLayout="table"
  maxHeight={400}
/>

Server-side pagination and search

See Server mode and Search Object. Minimal usage:

<DataTable
  rows={currentPageRows}
  columns={columns}
  loading={loading}
  search={{
    mode: 'server',
    value: searchValue,
    onChange: setSearchValue,
    onSubmit: (value) => {
      setPage(1);
      setAppliedSearch(value);
    },
  }}
  pagination={{
    mode: 'server',
    totalCount,
    page,
    pageSize,
    onPageChange: setPage,
    onPageSizeChange: (nextSize) => {
      setPageSize(nextSize);
      setPage(1);
    },
  }}
/>

Custom theme & chrome

<DataTable
  title="Customers"
  showSearch
  searchPlaceholder="Find a customer…"
  rows={rows}
  columns={columns}
  theme={{ '--table-theme-color': '#3b82f6' }}
  pagination={{ defaultPageSize: 25, pageSizeOptions: [10, 25, 50] }}
/>

Mobile behavior

  • ≤768px + mobileLayout="cards" (default): each row becomes a labeled card; action menu and selection sit in the card header.
  • ≤768px + mobileLayout="table": keeps the grid with horizontal scroll, sticky selection/first column, and compact pagination (First/Last hidden; fewer page buttons).
  • Columns with hideOnMobile: true are omitted on small screens in both modes.

Requirements

  • React: 16.8+ / 17+ / 18+ / 19+
  • React DOM: 16.8+ / 17+ / 18+ / 19+

Browser Support

  • Chrome, Firefox, Safari, Edge (latest)

License

MIT License — see License file for details.


Support