How to Implement Bulk Delete with Table Selection and bulkDeleteAction in refine-shadcn

To implement bulk delete in refine‑shadcn, create a factory function that receives the TanStack Table instance, extracts selected rows via table.getSelectedRowModel().rows, invokes the useDeleteHelper hook for each record, and passes the resulting { label, onClick } object to the options prop of Table.CheckAll located in packages/theme/src/table/fields/checkall.tsx.

The refine‑shadcn UI toolkit extends Refine with shadcn/ui components and provides a Table primitive that already handles row selection through the CheckAll component. Because the library exposes the full TanStack Table API alongside custom hooks like useDeleteHelper, implementing bulk deletion requires no changes to core components—only composition of existing utilities in your list page.

How Table Selection Works in refine‑shadcn

The selection UI is managed by CheckAll in packages/theme/src/table/fields/checkall.tsx. This component renders the master checkbox in the table header and exposes a dropdown menu that accepts an options array. Each option expects a label string and an onClick handler, allowing you to inject custom bulk actions such as “Delete Selected”.

Row‑level deletions are handled by DeleteAction in packages/theme/src/table/actions/delete.tsx, which consumes DeleteContext from packages/theme/src/providers/deleteProvider.tsx. The actual network logic lives in packages/theme/src/hooks/useDeleteHelper.tsx, a wrapper around Refine’s useDelete that enforces access control and mutation modes. Bulk deletion follows the same pattern: iterate over selected rows and call the helper for each ID.

Creating the bulkDeleteAction Factory

In your list page (for example, templates/vite-react/src/pages/users/list.tsx), define a factory that receives the table instance and returns an action object:

import { useContext } from "react";
import { DeleteContext, useDeleteHelper } from "@ferdiunal/refine-shadcn";
import { useUserFriendlyName } from "@refinedev/core";
import { UseTableReturnType, BaseRecord, HttpError } from "@refinedev/react-table";

const BulkDeleteAction = (table: UseTableReturnType<BaseRecord, HttpError>) => {
    const friendly = useUserFriendlyName();
    const selectedRows = table.getSelectedRowModel().rows;
    
    const label = `Delete Selected (${selectedRows.length}) ${friendly(
        "Row",
        selectedRows.length > 1 ? "plural" : "singular",
    )}`;

    const { mutate, isLoading } = useDeleteHelper("", "");

    const onClick = async () => {
        if (!selectedRows.length) return;
        
        if (!window.confirm(`Delete ${selectedRows.length} selected rows?`)) return;

        await Promise.all(
            selectedRows.map(row => 
                mutate({
                    resource: "users", // Replace with your resource name
                    id: row.original.id,
                })
            ),
        );

        table.toggleAllPageRowsSelected(false);
    };

    return { label, onClick, disabled: selectedRows.length === 0 || isLoading };
};

Key implementation details:

  • table.getSelectedRowModel().rows provides the array of selected row objects.
  • useDeleteHelper accepts empty strings for resource and ID because you will supply them per‑call inside the loop.
  • Promise.all executes deletions concurrently; alternatively, use for…of for sequential processing.
  • table.toggleAllPageRowsSelected(false) clears the selection state after the operation succeeds.

Wiring the Action to Table.CheckAll

Pass the factory to the options prop of Table.CheckAll inside your table definition:

import { ListPage, Table } from "@ferdiunal/refine-shadcn";
import { Checkbox } from "@/components/ui/checkbox";
import { BulkDeleteAction } from "./BulkDeleteAction";

export const UserList = () => {
    return (
        <ListPage>
            <Table enableSorting enableFilters>
                <Table.Column
                    accessorKey="id"
                    id="select"
                    header={({ table }) => (
                        <Table.CheckAll
                            options={[BulkDeleteAction(table)]}
                            table={table}
                        />
                    )}
                    cell={({ row }) => (
                        <Checkbox
                            checked={row.getIsSelected()}
                            onCheckedChange={value => row.toggleSelected(!!value)}
                        />
                    )}
                />
                <Table.Column accessorKey="firstName" header="First Name" />
                <Table.Column
                    accessorKey="id"
                    id="actions"
                    cell={({ row: { original } }) => (
                        <Table.Actions>
                            <Table.ShowAction resource="users" row={original} />
                            <Table.EditAction resource="users" row={original} />
                            <Table.DeleteAction resource="users" row={original} />
                        </Table.Actions>
                    )}
                />
            </Table>
        </ListPage>
    );
};

The options array accepts multiple bulk actions, so you can extend [BulkDeleteAction(table)] with additional handlers for export or status updates.

(Optional) Reusing the Delete Confirmation Modal for Bulk

To leverage the existing confirmation modal from DeleteProvider instead of window.confirm, extend the context type in packages/theme/src/providers/deleteProvider.tsx:

type DeleteDataType = {
    toogle: boolean;
    rows?: any[]; // Add this for bulk support
    resource: string;
    redirectBack?: boolean;
    onAfterHandle?: () => void;
};

Then update your bulk action to trigger the modal:

const deleteContext = useContext(DeleteContext);

const onClick = () => {
    deleteContext?.updateData({
        rows: selectedRows.map(r => r.original),
        resource: "users",
        toogle: true,
        onAfterHandle: () => table.toggleAllPageRowsSelected(false),
    });
};

You would then modify the modal content to render the row count and, on confirmation, iterate over rows to call mutate for each item, keeping the UI consistent with single‑row deletions.

Summary

  • CheckAll in packages/theme/src/table/fields/checkall.tsx accepts bulk actions through its options prop.
  • Access selected rows via table.getSelectedRowModel().rows provided by @refinedev/react-table.
  • Execute deletions with useDeleteHelper from packages/theme/src/hooks/useDeleteHelper.tsx to preserve access control and mutation modes.
  • Clear selection after deletion using table.toggleAllPageRowsSelected(false).
  • No backend changes are required because useDeleteHelper maps to the standard Refine DELETE /{resource}/{id} endpoint.

Frequently Asked Questions

How does Table.CheckAll receive custom bulk actions?

Table.CheckAll accepts an options prop that expects an array of objects shaped as { label: string, onClick: () => void, disabled?: boolean }. Each object renders as a dropdown item next to the master checkbox, allowing you to wire arbitrary bulk operations such as delete, export, or status change.

Can I delete selected rows sequentially instead of concurrently?

Yes. Replace Promise.all with a for…of loop inside the onClick handler. Sequential execution is useful when your API has strict rate limits or when deletions have side‑effects that require ordering, though it will take longer for large selections.

Where is the delete confirmation modal defined in refine-shadcn?

The confirmation modal is defined in packages/theme/src/providers/deleteProvider.tsx. It exposes DeleteContext, which is consumed by Table.DeleteAction and can also be imported into custom bulk actions if you want to reuse the same UI for confirming multi‑row deletions.

Why does my bulk action lose the table selection after navigation?

TanStack Table stores selection state in the component instance. If you navigate away and return, the table remounts and selection resets. To persist selection across sessions, pass a state.rowSelection controlled prop to the table and synchronize it with URL search params or local storage.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →