Recipes

Working patterns for common product needs. Each recipe is self-contained. Copy into your project and adjust.

Persist column widths

Pass onColumnResize to receive the settled width after every drag, and initialColumnWidths to hydrate those widths on mount. The callback receives the resized column's id, its new width in px, and the full widths map. Write it to localStorage, a database, or anywhere else.

localStorage

import { useState } from 'react';
import DataTable, { type TableColumn } from 'react-data-table-component';

const STORAGE_KEY = 'employees-table-widths';

function loadWidths(): Record<string, number> {
  try { return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}'); }
  catch { return {}; }
}

const columns: TableColumn<Employee>[] = [
  { id: 'name',   name: 'Name',   selector: r => r.name   },
  { id: 'salary', name: 'Salary', selector: r => r.salary, right: true },
];

export default function App() {
  const [initialWidths] = useState(loadWidths);

  return (
    <DataTable
      columns={columns}
      data={data}
      resizable
      initialColumnWidths={initialWidths}
      onColumnResize={(_id, _w, all) =>
        localStorage.setItem(STORAGE_KEY, JSON.stringify(all))
      }
    />
  );
}

Database / back-end

Swap the callback body to call your API instead. Wrap with a debounce if you want to batch rapid resizes into a single request.

import { useCallback, useEffect, useState } from 'react';
import DataTable from 'react-data-table-component';
import { debounce } from 'lodash-es';

export default function App({ userId }: { userId: string }) {
  const [initialWidths, setInitialWidths] = useState<Record<string, number>>({});

  useEffect(() => {
    api.getColumnPrefs(userId).then(setInitialWidths);
  }, [userId]);

  const handleResize = useCallback(
    debounce((_id: string | number, _w: number, all: Record<string | number, number>) => {
      api.saveColumnPrefs(userId, all);
    }, 500),
    [userId],
  );

  return (
    <DataTable
      columns={columns}
      data={data}
      resizable
      initialColumnWidths={initialWidths}
      onColumnResize={handleResize}
    />
  );
}

Server-side sort, page, and filter

Enable sortServer, paginationServer, and pass controlled filterValues. Refetch from a single effect that depends on every state knob:

import { useEffect, useState } from 'react';
import DataTable, { type FilterState, SortOrder, type TableColumn } from 'react-data-table-component';

export default function App() {
  const [rows, setRows]       = useState<Employee[]>([]);
  const [total, setTotal]     = useState(0);
  const [page, setPage]       = useState(1);
  const [perPage, setPerPage] = useState(20);
  const [sort, setSort]       = useState<{ id?: string; dir: SortOrder }>({ dir: SortOrder.ASC });
  const [filters, setFilters] = useState<Record<string, FilterState>>({});
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    fetchEmployees({ page, perPage, sort, filters }).then(res => {
      if (cancelled) return;
      setRows(res.rows);
      setTotal(res.total);
      setLoading(false);
    });
    return () => { cancelled = true; };
  }, [page, perPage, sort, filters]);

  const columns: TableColumn<Employee>[] = [
    { id: 'name',   name: 'Name',   selector: r => r.name,   sortable: true, filterable: true },
    { id: 'salary', name: 'Salary', selector: r => r.salary, sortable: true, filterable: true,
      filterType: 'number', right: true },
  ];

  return (
    <DataTable
      columns={columns}
      data={rows}
      progressPending={loading}

      pagination
      paginationServer
      paginationTotalRows={total}
      onChangePage={setPage}
      onChangeRowsPerPage={setPerPage}

      sortServer
      onSort={(col, dir) => setSort({ id: col.id as string, dir })}

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

Bulk-action toolbar

v8 removed the built-in contextMessage and contextActions props. The recommended replacement is a toolbar rendered outside the table, driven by onSelectedRowsChange. This gives you full control over layout, styling, and what actions appear.

Show the toolbar only when rows are selected, display a count and names, and use the imperative ref to clear selection after the action completes.

import { useRef, useState } from 'react';
import DataTable, { type DataTableHandle, type TableColumn } from 'react-data-table-component';

interface Employee { id: number; name: string; department: string; }

const columns: TableColumn<Employee>[] = [
  { name: 'Name',       selector: r => r.name },
  { name: 'Department', selector: r => r.department },
];

export default function App() {
  const ref = useRef<DataTableHandle>(null);
  const [selected, setSelected] = useState<Employee[]>([]);

  async function handleArchive() {
    await api.archive(selected.map(r => r.id));
    ref.current?.clearSelectedRows();
  }

  async function handleExport() {
    await api.export(selected.map(r => r.id));
    ref.current?.clearSelectedRows();
  }

  return (
    <div>
      {/* Toolbar — only visible when rows are selected */}
      {selected.length > 0 && (
        <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '8px 12px',
          background: '#eff6ff', borderRadius: 6, marginBottom: 8 }}>
          <span style={{ fontWeight: 500 }}>
            {selected.length} selected: {selected.map(r => r.name).join(', ')}
          </span>
          <div style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
            <button onClick={handleExport}>Export</button>
            <button onClick={handleArchive}>Archive</button>
            <button onClick={() => ref.current?.clearSelectedRows()}>Cancel</button>
          </div>
        </div>
      )}

      <DataTable
        ref={ref}
        columns={columns}
        data={data}
        keyField="id"
        selectableRows
        onSelectedRowsChange={({ selectedRows }) => setSelected(selectedRows)}
      />
    </div>
  );
}

Master/detail with a nested table

Render a second DataTable inside expandableRowsComponent:

import DataTable, { type ExpanderComponentProps, type TableColumn } from 'react-data-table-component';

interface Order { id: string; total: number; items: OrderItem[]; }
interface OrderItem { sku: string; qty: number; price: number; }

const itemColumns: TableColumn<OrderItem>[] = [
  { name: 'SKU',   selector: i => i.sku },
  { name: 'Qty',   selector: i => i.qty,   center: true, width: '80px' },
  { name: 'Price', selector: i => i.price, right: true,  width: '110px',
    format: i => `$${i.price.toFixed(2)}` },
];

function OrderDetail({ data }: ExpanderComponentProps<Order>) {
  return (
    <div style={{ padding: 12, background: '#f9fafb' }}>
      <DataTable columns={itemColumns} data={data.items} dense />
    </div>
  );
}

<DataTable
  columns={orderColumns}
  data={orders}
  expandableRows
  expandableRowsComponent={OrderDetail}
/>

Sticky footer with totals

Use the built-in footer — add a footer field on any column and the table renders an aligned totals row automatically. If you'd rather render your own summary outside the table, the pattern below still works:

import DataTable, { type TableColumn } from 'react-data-table-component';

const totals = data.reduce(
  (acc, r) => ({ salary: acc.salary + r.salary, headcount: acc.headcount + 1 }),
  { salary: 0, headcount: 0 },
);

<>
  <DataTable columns={columns} data={data} />
  <div style={{
    display: 'flex',
    padding: '8px 16px',
    background: '#f3f4f6',
    fontWeight: 600,
    borderTop: '1px solid #e5e7eb',
  }}>
    <span style={{ flex: 1 }}>{totals.headcount} employees</span>
    <span>Total: ${totals.salary.toLocaleString()}</span>
  </div>
</>

Excel-style editable grid

Combine editable: true, onCellEdit, and an immer-style reducer for a spreadsheet feel. See the inline editing docs for the full pattern.

const columns: TableColumn<Row>[] = [
  { id: 'name',     name: 'Name',     selector: r => r.name,     editable: true, onCellEdit },
  { id: 'quantity', name: 'Quantity', selector: r => r.quantity, editable: true, onCellEdit,
    right: true, width: '110px' },
  { id: 'price',    name: 'Price',    selector: r => r.price,    editable: true, onCellEdit,
    right: true, width: '110px',
    format: r => `$${r.price.toFixed(2)}` },
  { id: 'total',    name: 'Total',    selector: r => r.quantity * r.price,
    right: true, width: '110px',
    format: r => `$${(r.quantity * r.price).toFixed(2)}` },
];

Row grouping (manual)

The library doesn't include row grouping, but you can fake it by pre-grouping your data and using conditionalRowStyles to highlight group headers:

type Row = Employee | { __group: string };

const grouped: Row[] = [
  { __group: 'Engineering' },
  ...employees.filter(e => e.dept === 'Engineering'),
  { __group: 'Product' },
  ...employees.filter(e => e.dept === 'Product'),
];

<DataTable
  data={grouped}
  columns={[
    { name: 'Name',
      cell: row => '__group' in row
        ? <strong>{row.__group}</strong>
        : row.name,
    },
    // ... other columns ...
  ]}
  conditionalRowStyles={[
    { when: row => '__group' in row,
      style: { background: '#f3f4f6', fontWeight: 600 } },
  ]}
/>

CSV export

No built-in exporter. For typical sizes, a 20-line client-side function is fine:

function exportCsv<T>(rows: T[], columns: TableColumn<T>[], filename = 'export.csv') {
  const header = columns.map(c => JSON.stringify(c.name)).join(',');
  const lines = rows.map(row =>
    columns
      .map(c => JSON.stringify(c.selector ? String(c.selector(row)) : ''))
      .join(','),
  );
  const csv = [header, ...lines].join('\n');
  const blob = new Blob([csv], { type: 'text/csv' });
  const url = URL.createObjectURL(blob);
  const a = Object.assign(document.createElement('a'), { href: url, download: filename });
  a.click();
  URL.revokeObjectURL(url);
}

<button onClick={() => exportCsv(data, columns, 'employees.csv')}>Export</button>