DEV Community

Cover image for Data Table From Scratch. Part 9: Delete Rows
Dima Vyshniakov
Dima Vyshniakov

Posted on

Data Table From Scratch. Part 9: Delete Rows

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.

Deletion demo

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]);
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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,
})
Enter fullscreen mode Exit fullscreen mode

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.

Deletion dialog

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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}
/>
Enter fullscreen mode Exit fullscreen mode

Demo

Here is a working demo of this exercise.

To be continued.

Top comments (0)