Performance

react-data-table-component is heavily memoized. Cell and column components only re-render when their own props change. Most performance problems we see come from identity churn in props passed to DataTable, not from the library itself. This page covers the patterns that matter.

Row count guidance

The table does not virtualize rows. Every row in data renders to the DOM. As a rough guide:

Row countBehavior
< 500No performance considerations. Render as-is.
500 – 5,000Enable pagination with a reasonable paginationPerPage (25–100). Sort and filter stay fast.
5,000 – 50,000Use paginationServer and sortServer. Don't pass the full dataset to DataTable. Slice on the server and send only the visible page.
> 50,000Always server-side. Consider whether a table is the right UI at this scale. Search or visualization tools are usually a better fit.

Try it yourself

Don't take the table's word for it. This demo generates the dataset in your browser and times every interaction: data generation, sorting, filtering, and page changes. You can see exactly where client-side handling stops being free on your hardware.

Stress test

Pick a row and column count, then sort, filter, and paginate. The readout shows generation time and click-to-render time for the last interaction.

Rows
Columns
Features
10,000 rows × 6 cols (60,000 cells) · generated in 2ms · 25 rows in the DOM
ID
Name
Department
Region
Salary
Score
1
Jordan Morgan
Analytics
LATAM
$162,800
80.9
2
Sam Kim
Engineering
EMEA
$107,700
90.9
3
Morgan Kapoor
HR
APAC
$91,300
14.7
4
Drew Nguyen
HR
APAC
$71,100
82.5
5
Drew Rivera
Engineering
EMEA
$152,900
48.3
6
Sam Morgan
Engineering
APAC
$148,500
27.7
7
Morgan Kapoor
Product
EMEA
$145,200
93.4
8
Morgan Park
Design
NA
$47,200
10.8
9
Morgan Rivera
Analytics
APAC
$54,200
53
10
Alex Okafor
Design
APAC
$68,900
87.1
11
Jordan Park
Product
LATAM
$75,200
46.1
12
Alex Park
Analytics
NA
$117,500
95.5
13
Riley Rivera
Engineering
APAC
$82,600
1.4
14
Morgan Rivera
HR
EMEA
$114,600
36.6
15
Sam Ellis
HR
EMEA
$77,300
58.9
16
Taylor Okafor
Analytics
APAC
$107,200
74.4
17
Marcus Lee
HR
EMEA
$130,900
81.9
18
Morgan Okafor
Sales
EMEA
$80,000
23.6
19
Casey Morgan
Sales
EMEA
$137,800
78
20
Marcus Morgan
Analytics
EMEA
$92,600
86.4
21
Jamie Chen
Sales
EMEA
$84,400
41.8
22
Jamie Nguyen
HR
APAC
$71,300
64.6
23
Jordan Chen
HR
NA
$153,400
71
24
Aria Rivera
Product
EMEA
$118,500
62
25
Jordan Morgan
HR
APAC
$118,700
30.3

Sorting and filtering always run over the full dataset. Click a column header or filter Name at 100,000 rows to see the cost. Only the current page renders to the DOM; switch to 500 rows per page to stress actual DOM size. Each feature toggle remounts its extra cells and handlers, so flip them at 500 rows per page to see what they cost. Try dragging a column edge (Resizable) or a header (Column reorder), and use arrow keys after clicking a cell (Keyboard nav).

Recommendations

Everything in this section is in your hands as a consumer of the library. The table memoizes its own internals; these patterns keep your side of the contract so that memoization actually holds.

The biggest footgun: new references every render

DataTable uses React.memo on its row and cell components. Memoization is broken by reference equality, not value equality. These patterns silently re-render every row on every parent render:

// ❌ New array literal every render → all rows re-render
function App({ items }) {
  return (
    <DataTable
      columns={[
        { name: 'Name', selector: r => r.name }, // new function every render
      ]}
      data={items}
    />
  );
}

// ❌ Inline conditional row styles → new array → all rows re-render
<DataTable
  conditionalRowStyles={[{ when: r => r.flagged, style: { color: 'red' } }]}
  /* ... */
/>;

Fix: hoist or memoize the references.

// ✅ Stable column array
const columns: TableColumn<Item>[] = [{ name: 'Name', selector: r => r.name }];

function App({ items }) {
  return <DataTable columns={columns} data={items} />;
}

// ✅ Or memoize when it depends on state
const columns = useMemo<TableColumn<Item>[]>(
  () => [
    { name: 'Name', selector: r => r.name },
    { name: 'Salary', selector: r => r.salary, omit: !showSalary },
  ],
  [showSalary],
);

Stabilize event handlers

Pass useCallback-wrapped handlers for any callback prop you read from inside a row (onRowClicked, onSelectedRowsChange, etc.). Without it, every render changes the handler identity and the row context updates downstream.

const handleRowClicked = useCallback(
  (row: Employee) => {
    navigate(`/employees/${row.id}`);
  },
  [navigate],
);

<DataTable columns={columns} data={data} onRowClicked={handleRowClicked} />;

Stabilize the data array

A new data array re-runs sort, pagination slicing, and filter predicates. If your data comes from a server, store the response in state and only replace it on real updates. Don't recreate it during render.

// ❌ data is a new array every render
function App() {
  const items = useStore(s => s.items.map(transform)); // new array
  return <DataTable data={items} columns={columns} />;
}

// ✅ Compute once, memoize
function App() {
  const rawItems = useStore(s => s.items);
  const items = useMemo(() => rawItems.map(transform), [rawItems]);
  return <DataTable data={items} columns={columns} />;
}

Sorting is O(n log n) in the table

Client-side sort runs on the full data array on every sort change. For larger datasets, push sort to the server with sortServer. DataTable will skip its internal sort and call onSort(column, direction) instead.

<DataTable
  columns={columns}
  data={pageOfData}
  sortServer
  onSort={(column, direction) => refetch({ sortBy: column.id, sortDir: direction })}
/>;

Server-side pagination

When pagination is server-driven, pass only the current page's rows in dataand the total row count in paginationTotalRows. The table treatsdata as "this page" and lets the pagination footer drive page changes:

<DataTable
  columns={columns}
  data={currentPageRows} // just the rows for this page
  paginationServer
  paginationTotalRows={total} // total across all pages
  onChangePage={setPage}
  onChangeRowsPerPage={setPerPage}
  pagination
/>;

Column filtering

Built-in filters run client-side on every keystroke. For large datasets, switch to controlled filters and debounce server calls:

import { useState, useEffect } from 'react';

const [filters, setFilters] = useState({});
const [debounced, setDebounced] = useState(filters);

useEffect(() => {
  const t = setTimeout(() => setDebounced(filters), 250);
  return () => clearTimeout(t);
}, [filters]);

useEffect(() => {
  refetch({ filters: debounced });
}, [debounced]);

<DataTable
  columns={columns}
  data={data}
  filterValues={filters}
  onFilterChange={(columnId, next) => setFilters(prev => ({ ...prev, [columnId]: next }))}
/>;

Cell renderers

A custom cell renderer runs once per cell on every row re-render. Keep them cheap. Don't allocate inside them. Compute style objects outside the renderer if possible:

// ❌ New style object every cell
{
  cell: row => <span style={{ padding: 8, color: row.flagged ? 'red' : 'black' }}>{row.name}</span>;
}

// ✅ Static + conditional override
const baseStyle = { padding: 8 };
{
  cell: row => <span style={row.flagged ? { ...baseStyle, color: 'red' } : baseStyle}>{row.name}</span>;
}

Avoid expensive selectors

selector runs once per cell on render and once per row on every sort or filter. Keep them O(1):

// ❌ Walks an array every call
{
  selector: r => r.tags.find(t => t.primary)?.name ?? '';
}

// ✅ Compute upstream, store on the row
const enriched = useMemo(() => rows.map(r => ({ ...r, primaryTag: r.tags.find(t => t.primary)?.name ?? '' })), [rows]);
{
  selector: r => r.primaryTag;
}

Animations

animateRows staggers row entrance and animates sort transitions. On very long pages (hundreds of rows) the cumulative animation cost can be visible. The animation is automatically disabled when the user has prefers-reduced-motion enabled.

Profiler checklist

If the table feels slow, open React DevTools → Profiler and check:

  1. Are individual row components re-rendering on parent-state changes that shouldn't affect them? → Some prop is changing identity. Check columns, conditionalRowStyles, callbacks.
  2. Is the body re-rendering on every keystroke in a controlled input outside the table? → Lift the keystroke state higher or memoize the table.
  3. Is sorting slow on click? → Move to sortServer.
  4. Is pagination slow on page change? → You're probably recomputing data every render. Memoize it.