Categories
Uncategorized

React Table Library: The Complete Developer’s Guide to Building Advanced Data Tables






React Table Library: Complete Guide to Setup, Sorting & Filtering




React Table Library: The Complete Developer’s Guide to Building Advanced Data Tables

Updated July 2025  ·  12 min read  ·  Covers: installation, sorting, filtering, pagination, selection, TypeScript

If you’ve spent more than thirty minutes staring at a blank <table> tag in React, wondering why something so conceptually simple turns into a state-management nightmare the moment a user wants to sort a column — you’re not alone. Data tables are deceptively complex. They sit at the intersection of UI rendering performance, async data fetching, user interaction state, and accessibility, all at the same time. React Table Library was built specifically for this problem space.

This guide walks you through everything — from react-table-library installation to advanced feature composition including sorting, filtering, pagination, and row selection. We’ll look at real code, explain the mental model behind the library, and cover patterns that actually survive contact with a production codebase.

No filler, no “just wrap everything in a useMemo” handwaving. Let’s build tables that work.

What Is React Table Library and Why Should You Care?

React Table Library (@table-library/react-table-library) is a modular React data table component that ships with both UI and logic — a deliberate middle ground between a raw headless utility like TanStack Table and a fully opinionated component suite like AG Grid Community. The library gives you a working, themeable table out of the box while keeping every feature layer independently composable via hooks.

The distinction matters in practice. Headless libraries give you maximum control but demand that you build every visual layer from scratch — sorting arrows, pagination UI, selection checkboxes, keyboard navigation. Full-featured grids hand you all of that, but customizing the internals often feels like defusing a bomb. React Table Library occupies the productive middle: its useSort, useFilter, usePagination, and useRowSelect hooks integrate cleanly with its default UI, but you can swap the theme, override components, or drive state externally for server-side control.

It’s also genuinely lightweight. The core package weighs in under 30 KB gzipped, feature plugins are imported separately, and it carries zero mandatory peer dependencies beyond React itself. For an enterprise React data grid or a simple interactive table in a dashboard, that combination of flexibility and low overhead is difficult to beat.

Installation and Project Setup

Getting started with react-table-library setup is intentionally low-friction. The core package and any feature modules you need are published separately under the @table-library scope, so your bundle only ever pays for what you actually use. Start with a fresh or existing React project — the library works identically with Create React App, Vite, and Next.js.

# Core package — always required
npm install @table-library/react-table-library

# Optional feature plugins (install only what you need)
npm install @table-library/react-table-library
# sorting, filtering, pagination, selection are all part of the core package
# Theme presets live here:
npm install @table-library/react-table-library

As of the current major version, all feature hooks (useSort, useFilter, usePagination, useRowSelect) are exported directly from @table-library/react-table-library. There’s no separate install step per feature — you simply import the hook you need. The library requires React 16.8+ for hooks support and works cleanly with React 18’s concurrent rendering model.

Once installed, your minimal working table needs three things: a data object with a nodes array, the Table component as your root, and at least Column definitions inside a Header and Body. The data contract is deliberately simple — every row needs a unique id field, and everything else is yours to define. There’s no column configuration object to pass to a top-level hook; you define columns declaratively in JSX, which makes dynamic column lists straightforward to implement with a plain .map().

import {
  Table,
  Header,
  HeaderRow,
  HeaderCell,
  Body,
  Row,
  Cell,
} from "@table-library/react-table-library/table";
import { useTheme } from "@table-library/react-table-library/theme";
import { DEFAULT_OPTIONS, getTheme } from "@table-library/react-table-library/material-ui";

const data = {
  nodes: [
    { id: "1", name: "VSCode", language: "TypeScript", stars: 152000 },
    { id: "2", name: "React",  language: "JavaScript", stars: 218000 },
    { id: "3", name: "Rust",   language: "Rust",       stars: 87000  },
  ],
};

export default function BasicTable() {
  const theme = useTheme(getTheme(DEFAULT_OPTIONS));

  return (
    <Table data={data} theme={theme}>
      {(tableList) => (
        <>
          <Header>
            <HeaderRow>
              <HeaderCell>Name</HeaderCell>
              <HeaderCell>Language</HeaderCell>
              <HeaderCell>Stars</HeaderCell>
            </HeaderRow>
          </Header>
          <Body>
            {tableList.map((item) => (
              <Row key={item.id} item={item}>
                <Cell>{item.name}</Cell>
                <Cell>{item.language}</Cell>
                <Cell>{item.stars.toLocaleString()}</Cell>
              </Row>
            ))}
          </Body>
        </>
      )}
    </Table>
  );
}

The render-prop pattern ({(tableList) => ...}) is central to how React Table Library works. The library processes your raw data.nodes through whatever feature hooks are active and hands you back the derived list to render. This keeps your JSX in full control of the row structure while the library handles the stateful logic — a clean separation that makes react-table-library advanced use cases feel natural rather than forced.

Sorting: Making Columns Clickable Without Losing Your Mind

React-table-library sorting is implemented via the useSort hook, and it follows the same pattern as every other feature hook in this library — you initialize state, pass it to <Table>, and the hook feeds sorted data into your render prop automatically. What makes it genuinely developer-friendly is that the sort function lives on each column definition independently, so string sort, numeric sort, and date sort can all coexist in the same table without any adapter gymnastics.

import { useSort, HeaderCellSort } from "@table-library/react-table-library/sort";

export default function SortableTable() {
  const sort = useSort(
    data,
    { onChange: onSortChange },
    {
      sortFns: {
        NAME:  (array) => array.sort((a, b) => a.name.localeCompare(b.name)),
        STARS: (array) => array.sort((a, b) => a.stars - b.stars),
      },
    }
  );

  function onSortChange(action, state) {
    // state = { sortKey, reverse }
    // trigger server fetch here for server-side sorting
    console.log(action, state);
  }

  return (
    <Table data={data} sort={sort}>
      {(tableList) => (
        <>
          <Header>
            <HeaderRow>
              <HeaderCellSort sortKey="NAME">Name</HeaderCellSort>
              <HeaderCell>Language</HeaderCell>
              <HeaderCellSort sortKey="STARS">Stars</HeaderCellSort>
            </HeaderRow>
          </Header>
          <Body>
            {tableList.map((item) => (
              <Row key={item.id} item={item}>
                <Cell>{item.name}</Cell>
                <Cell>{item.language}</Cell>
                <Cell>{item.stars.toLocaleString()}</Cell>
              </Row>
            ))}
          </Body>
        </>
      )}
    </Table>
  );
}

The onChange callback receives both the dispatched action and the current sort state object. This is your integration point for server-side sorting — when the sort state changes, fire your data-fetch function with the new sortKey and reverse flag, update your data source, and the table re-renders with the new results. The library doesn’t care whether the sorted data came from client-side logic or a remote API. It processes whatever is in data.nodes at render time.

Multi-column sort is supported. Pass an initial state with multiple sort keys and the library maintains sort priority. For custom sort indicators — if the default chevron icons don’t match your design system — HeaderCellSort accepts a sortIcon prop that takes a render function, giving you full control over the ascending/descending/neutral visual states without overriding any CSS internals.

Filtering: Search, Conditions, and Controlled State

React-table-library filtering works on the same mental model: a useFilter hook produces a filter state object that you pass to <Table>, and the library applies your filter function to data.nodes before handing results to the render prop. The filter function is entirely your responsibility — the library provides the state machinery and the integration plumbing, but the predicate logic stays in your code. That’s the right trade-off.

import { useFilter } from "@table-library/react-table-library/filter";
import { useState } from "react";

export default function FilterableTable() {
  const [search, setSearch] = useState("");

  const filter = useFilter(
    data,
    { onChange: onFilterChange },
    {
      filterFn: (item) =>
        item.name.toLowerCase().includes(search.toLowerCase()) ||
        item.language.toLowerCase().includes(search.toLowerCase()),
    }
  );

  function onFilterChange(action, state) {
    console.log(action, state);
  }

  return (
    <>
      <input
        type="search"
        placeholder="Filter repositories…"
        value={search}
        onChange={(e) => {
          setSearch(e.target.value);
          filter.fns.onAddFilterByFn("SEARCH");
        }}
      />
      <Table data={data} filter={filter}>
        {(tableList) => (
          <>
            {/* Header and Body as before */}
          </>
        )}
      </Table>
    </>
  );
}

One nuance worth understanding: the filter hook re-evaluates filterFn on every render by design. If your filter involves an expensive computation (fuzzy matching a large dataset, for instance), wrap the predicate result in a useMemo keyed to your filter input. For most real-world cases with datasets under a few thousand rows, the default behavior is perfectly performant — the library does not do unnecessary re-renders when filter state hasn’t changed.

For column-specific filters — where each column header contains its own filter input — you can compose multiple useFilter hook instances or maintain a single filter state object that your predicate inspects at multiple paths. The latter scales better because it keeps the table receiving a single filter prop. If you’re building an enterprise React data grid with a complex filter panel, consider housing filter state in a useReducer outside the table and deriving a single compound predicate from it. The library accommodates this pattern without any special configuration.

Pagination: Client-Side and Server-Side, Both Done Right

React Table Library pagination is controlled through usePagination, and it’s one of the areas where the library’s design philosophy really pays off. The hook manages page index and page size state; you provide the total record count (necessary for server-side scenarios); the hook computes total pages, exposes navigation functions, and feeds the correct slice of nodes to your render prop automatically in client-side mode.

import { usePagination, PaginationButton } from "@table-library/react-table-library/pagination";

export default function PaginatedTable() {
  const pagination = usePagination(data, {
    state: { page: 0, size: 10 },
    onChange: onPaginationChange,
  });

  function onPaginationChange(action, state) {
    // For server-side: fetch page state.page with size state.size
    console.log(action, state);
  }

  return (
    <>
      <Table data={data} pagination={pagination}>
        {(tableList) => (
          <>
            {/* Header and Body */}
          </>
        )}
      </Table>

      <div style={{ display: "flex", justifyContent: "center", gap: "0.5rem", marginTop: "1rem" }}>
        <PaginationButton
          onClick={() => pagination.fns.onSetPage(0)}
          disabled={pagination.state.page === 0}
        >
          First
        </PaginationButton>

        {Array.from({ length: pagination.state.getTotalPages(data.nodes) }, (_, i) => (
          <PaginationButton
            key={i}
            onClick={() => pagination.fns.onSetPage(i)}
            isActive={pagination.state.page === i}
          >
            {i + 1}
          </PaginationButton>
        ))}

        <PaginationButton
          onClick={() =>
            pagination.fns.onSetPage(
              pagination.state.getTotalPages(data.nodes) - 1
            )
          }
          disabled={
            pagination.state.page ===
            pagination.state.getTotalPages(data.nodes) - 1
          }
        >
          Last
        </PaginationButton>
      </div>
    </>
  );
}

For server-side pagination, the pattern shifts slightly. Your data.nodes should already contain only the current page’s records — the server pre-sliced them. You need to pass a total value to the hook so it can calculate total pages correctly without client-side data to count. Set serverSide: true in the hook options and drive all data fetching from the onChange callback. The hook handles no client-side slicing in this mode; it purely manages page state and fires your callback when it changes.

Page size controls — the “Rows per page” dropdown common in material-design tables — are wired through pagination.fns.onSetSize(newSize). That’s a plain function call; you build whatever UI select element fits your design system and call it on change. The library resets the current page to zero automatically when size changes, which is the correct behavior and saves you from a subtle bug that bites many hand-rolled pagination implementations.

Row Selection: Single, Multi, and Checkbox Patterns

React-table-library selection is implemented via useRowSelect and covers three distinct interaction modes: single selection (radio-button semantics, one row at a time), multi-selection (independent checkboxes), and select-all with indeterminate state for the header checkbox. All three are driven from the same hook — the difference is in the SELECT_TYPES constant you pass.

import {
  useRowSelect,
  HeaderCellSelect,
  CellSelect,
  SELECT_ALL_MODES,
} from "@table-library/react-table-library/select";

export default function SelectableTable() {
  const select = useRowSelect(data, {
    onChange: onSelectChange,
  });

  function onSelectChange(action, state) {
    // state.ids = array of selected row IDs
    console.log("Selected IDs:", state.ids);
  }

  return (
    <Table data={data} select={select}>
      {(tableList) => (
        <>
          <Header>
            <HeaderRow>
              <HeaderCellSelect selectAllMode={SELECT_ALL_MODES.ALL} />
              <HeaderCell>Name</HeaderCell>
              <HeaderCell>Language</HeaderCell>
              <HeaderCell>Stars</HeaderCell>
            </HeaderRow>
          </Header>
          <Body>
            {tableList.map((item) => (
              <Row key={item.id} item={item}>
                <CellSelect item={item} />
                <Cell>{item.name}</Cell>
                <Cell>{item.language}</Cell>
                <Cell>{item.stars.toLocaleString()}</Cell>
              </Row>
            ))}
          </Body>
        </>
      )}
    </Table>
  );
}

The state.ids array in onChange is your integration point to the rest of the application — feed it to a bulk-action toolbar, a delete confirmation dialog, or a form submit handler. The library does not try to own what you do with selected IDs, which is exactly correct. For programmatic selection (pre-selecting rows on load based on previously saved state), initialize the hook with an initialState: { ids: ["1", "3"] } option.

A common enterprise requirement is cross-page selection persistence — a user selects rows on page 1, navigates to page 2, selects more, and expects both sets to appear in the final selection. The library supports this when you compose useRowSelect with usePagination: selection state persists across page changes by default because it lives in its own hook scope, independent of what the pagination hook is currently displaying. No extra configuration required — it just works, which is refreshingly rare in this problem domain.

Composing Features: The Real Power of React Table Library

The genuine strength of react-table-library advanced usage is feature composition — using sorting, filtering, pagination, and selection simultaneously in a single table, with each feature hook operating independently and the library orchestrating the data pipeline between them. When you pass multiple feature objects to <Table>, the library applies them in a defined order: filter first, then sort, then paginate. Selection state is orthogonal and doesn’t affect the data pipeline.

// Full feature composition
export default function EnterpriseTable() {
  const [search, setSearch] = useState("");

  const filter = useFilter(data, {}, {
    filterFn: (item) => item.name.toLowerCase().includes(search.toLowerCase()),
  });

  const sort = useSort(data, {}, {
    sortFns: {
      NAME:  (arr) => arr.sort((a, b) => a.name.localeCompare(b.name)),
      STARS: (arr) => arr.sort((a, b) => a.stars - b.stars),
    },
  });

  const pagination = usePagination(data, {
    state: { page: 0, size: 5 },
  });

  const select = useRowSelect(data, {
    onChange: (action, state) => console.log(state.ids),
  });

  return (
    <Table data={data} filter={filter} sort={sort} pagination={pagination} select={select}>
      {(tableList) => (
        <>
          {/* Full Header + Body composition */}
        </>
      )}
    </Table>
  );
}
Important: When filter or sort state changes in a composed table, usePagination automatically resets the page index to zero. This prevents the classic bug where a user filters results down to 2 items but the pagination still thinks it’s on page 5.

Theming composed tables is equally straightforward. The useTheme hook accepts a plain CSS-in-JS object where keys correspond to component selectors the library exposes. Material UI, Chakra UI, and Bootstrap theme presets are available as official packages. For custom design systems, the base theme object is well-documented and can be built incrementally — start with a border color and row hover state, and add specificity as your design requirements grow.

For truly large datasets — tens of thousands of rows — React Table Library integrates with virtual scrolling via react-window or react-virtual. The library docs provide an explicit integration example, but the core idea is simple: replace the Body child renderer with a virtualized list component and pass it the tableList array from the render prop. Column widths need to be fixed in virtual mode (as they do with any virtualized table), but everything else — sort, filter, selection — continues to work without modification.

TypeScript Integration and Production Patterns

React Table Library ships with comprehensive TypeScript definitions — not as a @types/ package, but bundled directly. Every hook is generic over your data shape, so you get full autocomplete and compile-time checking on node properties within sort functions, filter predicates, and cell renderers. The basic pattern is to define your row type and pass it as a type argument to useSort<MyRowType>, useFilter<MyRowType>, and so forth.

interface Repository {
  id: string;
  name: string;
  language: string;
  stars: number;
}

const typedData: TableNode<Repository>[] = [
  { id: "1", name: "VSCode", language: "TypeScript", stars: 152000 },
];

const sort = useSort<Repository>(
  { nodes: typedData },
  {},
  {
    sortFns: {
      // TypeScript knows `a` and `b` are Repository
      STARS: (arr) => arr.sort((a, b) => a.stars - b.stars),
    },
  }
);

In production, the patterns that tend to cause issues are: forgetting to stabilize data references (always memoize the data object with useMemo when it’s derived from props or API responses), passing unstable callback references to onChange handlers (wrap them in useCallback), and initializing hooks inside conditional branches (hooks are hooks — the usual React rules apply). None of these are library-specific gotchas; they’re standard React performance hygiene that becomes visible when a data table re-renders on every keystroke.

For accessibility, the library renders a semantic <table> element with appropriate <thead>, <tbody>, <tr>, and <td>/<th> elements. ARIA attributes for sort state (aria-sort) are managed automatically on HeaderCellSort. Checkbox inputs in CellSelect carry correct aria-label attributes. The result passes standard accessibility audits without requiring manual ARIA plumbing, which matters when your React interactive table needs to pass a WCAG 2.1 AA review.

Frequently Asked Questions

What is React Table Library and how does it differ from react-table (TanStack Table)?

React Table Library (@table-library/react-table-library) is a table component that ships with both rendering UI and stateful logic hooks, making it usable out of the box without building a visual layer from scratch. TanStack Table (formerly react-table v7/v8) is intentionally headless — it provides zero UI and expects you to construct every visual element yourself. React Table Library is the better choice when you want a working, themeable table quickly; TanStack Table wins when you need to embed table behavior into a highly custom design system where you control every rendered element.

Does React Table Library support server-side sorting and pagination?

Yes, fully. Every feature hook (useSort, useFilter, usePagination) exposes an onChange callback that fires with the current feature state whenever a user interaction changes it. In server-side mode, you use that callback to trigger your API fetch — passing sort key, sort direction, page index, and page size as query parameters. Update data.nodes with the API response, and the table re-renders with the new records. The library performs no client-side sorting or slicing when you’re driving data from the server.

Can I use React Table Library with TypeScript?

Absolutely — TypeScript support is first-class and bundled directly with the package. All hooks are generic over your data row type: pass your interface as a type argument to useSort<T>, useFilter<T>, usePagination<T>, and useRowSelect<T>, and you get full autocomplete and type-checking inside sort functions, filter predicates, and cell renderers. There is no separate @types/ package to install.


Leave a Reply

Your email address will not be published. Required fields are marked *