This is a single article from a series about creating of an advanced Data table component using React, TanStack Table 8, Tailwind CSS and Headless UI. In this chapter we focus on table editing capabilities
Welcome to the Data table workshop. In the previous exercise, we implemented row selection. Now, we'll apply that functionality to build one of the most common table interactions: row deletion.
We will implement the following workflow:
- User selects certain rows (e.g., result of filtering).
- User clicks the delete button.
- User confirms the choice in a dialog.
Implement row deletion logic
To make the table's data editable, we must first stop treating it as a static prop and manage it as internal state. The best way to encapsulate this logic is by creating a custom React hook at src/DataTable/features/useTableData.ts
.
The hook receives the four props:
-
tableDataProp: Row[]
: all table rows; -
rowSelection: RowSelectionState
: informs the hook about current selection; -
clearSelection: () => void
: helper to reset the selection UI; -
onEdit: (editState: EditState) => void
: callback to inform which rows are edited.
Inside the useTableData
hook, we first clone the incoming tableDataProp
into local state. We use an arrow function inside useState
to ensure this cloning operation runs only once on the initial render, not on every re-render. An accompanying useEffect
hook keeps our local state synchronized if the tableDataProp
changes from an external source, like a new data fetch.
const [tableData, setTableData] = useState(() => [...tableDataProp]);
useEffect(() => {
setTableData(() => [...tableDataProp]);
}, [tableDataProp]);
deleteRows
function
The core of our new hook is the deleteRows
function. It orchestrates the entire deletion process in a few clear steps.
First, it converts the rowSelection
object (e.g., { '2': true, '5': true }
) into an array of numeric row indices. For efficient lookup, this array is then converted into a Set
to achieve optimal O-complexity of the function. Then we filter the tableData
array, keeping only the rows whose indices are not in the rowsToDelete
set. This new, filtered array becomes our nextTableData
.
After updating the local state with setTableData
, we build an EditState
map to inform the parent component which rows were removed. Finally, we call clearSelection
to reset the UI, unchecking the checkboxes for the now-deleted rows.
const deleteRows = useCallback(() => {
const normalizedRows = Object.keys(rowSelection).map((rowIndex) =>
Number(rowIndex),
);
const rowsToDelete = new Set(normalizedRows);
const nextTableData = tableData.filter((_, i) => !rowsToDelete.has(i));
setTableData(nextTableData);
const editState: EditState = Object.fromEntries(
normalizedRows.map(rowIndex => [rowIndex, false])
);
onEdit(editState);
clearSelection();
}, [clearSelection, onEdit, rowSelection, tableData]);
Integrate hook to DataTable
component
With our hook ready, let's integrate it into the main src/DataTable/DataTable.tsx
component. We'll start by adding a new optional prop, onTableEdit
, to allow parent components to react to data changes.
type Props = {
//...
/**
* Callback to capture table data changes
* @see RowSelectionState
*/
onTableEdit?: (editState: EditState) => void;
};
Next, we'll call our new hook inside the DataTable
component. Notice the clean separation of concerns: useRowSelection
manages which rows are selected, while useTableData
handles the data manipulation. By passing handleClearSelection
as a callback, we allow the data hook to control the selection UI without being directly coupled to it. This keeps our logic pure and more testable.
Finally, we pass the stateful tableData
from our hook directly into the useReactTable
instance instead of the original prop.
const { rowSelection, handleRowSelection, handleClearSelection } =
useRowSelection({
rowSelectionProp,
onRowSelect,
});
const { tableData, deleteRows } = useTableData({
tableDataProp,
rowSelection,
clearSelection: handleClearSelection,
onEdit: onTableEdit
});
const table = useReactTable({
//...
data: tableData,
})
Create deletion UI
Now for the user-facing part. To prevent accidental deletions, we'll create a confirmation dialog at src/DataTable/dialogs/DeleteDialog.tsx
.
Outside a standard dialog properties DeleteDialog
receives rowsAmount
to display how many rows are selected for deletion, and onDelete
: callback that will trigger deleteRows
function.
Inside the component, the delete click handler invokes both onDelete
and onClose
callbacks.
export const DeleteDialog: FC<Props> = ({
locale,
rowsAmount,
isOpen,
onClose,
onDelete,
}) => {
const handleDelete = useCallback(() => {
onDelete();
onClose();
}, [onClose, onDelete]);
return (
<TableDialog title="Confirm deletion" open={isOpen} onClose={onClose}>
<div className="text-white/80">
Do you want to delete{' '}
<span className="font-semibold tabular-nums">
{formatRowsAmount(rowsAmount, locale)}
</span>{' '}
row(s)?
</div>
<div className="mt-6 flex justify-evenly gap-3">
<Button
className="min-w-32"
onClick={onClose}
title="Cancel"
icon="hand-palm"
/>
<Button
className="min-w-32"
onClick={handleDelete}
title="Delete"
icon="trash"
/>
</div>
</TableDialog>
);
};
Connect DeleteDialog
with DataTable
DeleteDialog
is built to consume deleteRows
and handleClearSelection
functions from the hooks we updated.
<DeleteDialog
onDelete={deleteRows}
onClose={closeDeleteDialog}
rowsAmount={selectedRows.length}
/>
Demo
Here is a working demo of this exercise.
To be continued.
Top comments (0)