Editable inventory grid
A line-item grid you edit directly, like a lightweight spreadsheet: product name and quantity are editable inline, a total column is derived automatically, and a footer bar keeps a running grand total. No form modal, no separate edit view.
Editable inventory grid
Edit product, quantity, or price directly in the cells. Total and grand total update automatically.
import React, { useState } from 'react';
import DataTable, { type TableColumn } from 'react-data-table-component';
interface LineItem {
id: number;
product: string;
quantity: number;
price: number;
}
const initialData: LineItem[] = [
{ id: 1, product: 'Widget A', quantity: 4, price: 12.5 },
{ id: 2, product: 'Widget B', quantity: 2, price: 34.0 },
{ id: 3, product: 'Gadget Pro', quantity: 1, price: 89.99 },
{ id: 4, product: 'Connector X', quantity: 10, price: 5.25 },
{ id: 5, product: 'Cable Pack', quantity: 3, price: 18.0 },
];
export default function EditableGridDemo() {
const [data, setData] = useState<LineItem[]>(initialData);
const handleCellEdit = (row: LineItem, value: string, column: TableColumn<LineItem>) => {
const field = column.id as keyof LineItem;
setData(prev =>
prev.map(r => {
if (r.id !== row.id) return r;
if (field === 'quantity') return { ...r, quantity: Math.max(0, parseInt(value) || 0) };
if (field === 'price') return { ...r, price: Math.max(0, parseFloat(value) || 0) };
return { ...r, [field]: value };
}),
);
};
const columns: TableColumn<LineItem>[] = [
{
id: 'product',
name: 'Product',
selector: r => r.product,
editable: true,
onCellEdit: handleCellEdit,
width: '160px',
},
{
id: 'quantity',
name: 'Qty',
selector: r => r.quantity,
editable: true,
editor: { type: 'number' },
onCellEdit: handleCellEdit,
right: true,
width: '100px',
},
{
id: 'price',
name: 'Unit price',
selector: r => r.price,
editable: true,
editor: { type: 'number' },
onCellEdit: handleCellEdit,
format: r => `$${r.price.toFixed(2)}`,
right: true,
width: '120px',
},
{
id: 'total',
name: 'Total',
selector: r => r.quantity * r.price,
format: r => `$${(r.quantity * r.price).toFixed(2)}`,
right: true,
width: '120px',
style: { fontWeight: 600, color: '#1d4ed8' },
},
];
const grandTotal = data.reduce((sum, r) => sum + r.quantity * r.price, 0);
return (
<div className="rounded-xl border border-gray-200 overflow-hidden">
<DataTable columns={columns} data={data} highlightOnHover noDataComponent="No items" />
<div className="flex justify-between items-center px-4 py-3 bg-gray-50 border-t border-gray-200 text-sm font-semibold">
<span className="text-gray-500">{data.length} items</span>
<span className="text-gray-900">
Grand total: <span className="text-brand-600">${grandTotal.toFixed(2)}</span>
</span>
</div>
</div>
);
}This demo uses Tailwind utility classes for layout — swap in your own CSS when copying into your project.
Editable columns with a shared handler
Set editable per column and point every editable column at the sameonCellEdit handler. Branch on the column's id to validate and coerce the incoming value:
function handleCellEdit(row: LineItem, value: string, column: TableColumn<LineItem>) {
const field = column.id as keyof LineItem;
setData(prev => prev.map(r => {
if (r.id !== row.id) return r;
if (field === 'quantity') return { ...r, quantity: Math.max(0, parseInt(value) || 0) };
if (field === 'price') return { ...r, price: Math.max(0, parseFloat(value) || 0) };
return { ...r, [field]: value };
}));
}
const columns: TableColumn<LineItem>[] = [
{ id: 'product', name: 'Product', selector: r => r.product, editable: true, onCellEdit: handleCellEdit },
{ id: 'quantity', name: 'Qty', selector: r => r.quantity, editable: true, editor: { type: 'number' }, onCellEdit: handleCellEdit, right: true },
{ id: 'price', name: 'Unit price', selector: r => r.price, editable: true, editor: { type: 'number' }, onCellEdit: handleCellEdit,
format: r => `$${r.price.toFixed(2)}`, right: true },
];A derived column that isn't editable
The total column has no editable flag — it's computed straight from the row on every render, so it never drifts out of sync with quantity or price:
{
id: 'total',
name: 'Total',
selector: r => r.quantity * r.price,
format: r => `$${(r.quantity * r.price).toFixed(2)}`,
right: true,
}Running grand total
Reduce over data on every render and show it in a footer bar below the table — it updates automatically as cells are edited, with no extra event wiring:
const grandTotal = data.reduce((sum, r) => sum + r.quantity * r.price, 0);
<div>
<DataTable columns={columns} data={data} />
<div className="total-bar">Grand total: ${grandTotal.toFixed(2)}</div>
</div>Prefer it inside the table itself? Pass a footer to a column instead — seeFooter for the built-in summary row.