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>