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
Render a toolbar above the table that appears when one or more rows are selected. Use the imperative ref to clear the selection after the action completes.
import { useRef, useState } from 'react';
import DataTable, { type DataTableHandle } from 'react-data-table-component';
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();
}
return (
<>
{selected.length > 0 && (
<div className="toolbar">
<span>{selected.length} selected</span>
<button onClick={handleArchive}>Archive</button>
<button onClick={() => ref.current?.clearSelectedRows()}>Cancel</button>
</div>
)}
<DataTable
ref={ref}
columns={columns}
data={data}
selectableRows
onSelectedRowsChange={({ selectedRows }) => setSelected(selectedRows)}
/>
</>
);
} 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
There's no built-in footer slot. The simplest pattern is to render a totals row outside the table that aligns to the same column layout:
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>