ra-soft-delete

react-admin ≥ 5.9.0

Keep deleted records to recover them later on. List all deleted records, inspect them, and restore or delete items permanently.

A deleted records list

Installation

npm install --save @react-admin/ra-soft-delete
# or
yarn add @react-admin/ra-soft-delete

Tip: ra-soft-delete is part of the React-Admin Enterprise Edition, and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package.

Data Provider

ra-soft-delete relies on the dataProvider to soft-delete, restore or view deleted records. In order to use the ra-soft-delete, you must add a few new methods to your data provider:

  • softDelete performs the soft deletion of the provided record.
  • softDeleteMany performs the soft deletion of the provided records.
  • getOneDeleted gets one deleted record by its ID.
  • getListDeleted gets a list of deleted records with filters and sort.
  • restoreOne restores a deleted record.
  • restoreMany restores deleted records.
  • hardDelete permanently deletes a record.
  • hardDeleteMany permanently deletes many records.
  • (OPTIONAL) createMany creates multiple records at once. This method is used internally by some data provider implementations to delete or restore multiple records at once. As it is optional, a default implementation is provided that simply calls create multiple times.
const dataProviderWithSoftDelete: SoftDeleteDataProvider = {
    ...dataProvider,

    softDelete: (resource, params: SoftDeleteParams): SoftDeleteResult => {
        const { id, authorId } = params;
        // ...
        return { data: deletedRecord };
    },
    softDeleteMany: (resource, params: SoftDeleteManyParams): SoftDeleteManyResult => {
        const { ids, authorId } = params;
        // ...
        return { data: deletedRecords };
    },

    getOneDeleted: (params: GetOneDeletedParams): GetOneDeletedResult => {
        const { id } = params;
        // ...
        return { data: deletedRecord };
    },
    getListDeleted: (params: GetListDeletedParams): GetListDeletedResult => {
        const { filter, sort, pagination } = params;
        // ...
        return { data: deletedRecords, total: deletedRecords.length };
    },

    restoreOne: (params: RestoreOneParams): RestoreOneResult => {
        const { id } = params;
        // ...
        return { data: deletedRecord };
    },
    restoreMany: (params: RestoreManyParams): RestoreManyResult => {
        const { ids } = params;
        // ...
        return { data: deletedRecords };
    },

    hardDelete: (params: HardDeleteParams): HardDeleteResult => {
        const { id } = params;
        // ...
        return { data: deletedRecordId };
    },
    hardDeleteMany: (params: HardDeleteManyParams): HardDeleteManyResult => {
        const { ids } = params;
        // ...
        return { data: deletedRecordsIds };
    },
};
const dataProviderWithSoftDelete = {
    ...dataProvider,

    softDelete: (resource, params) => {
        const { id, authorId } = params;
        // ...
        return { data: deletedRecord };
    },
    softDeleteMany: (resource, params) => {
        const { ids, authorId } = params;
        // ...
        return { data: deletedRecords };
    },

    getOneDeleted: (params) => {
        const { id } = params;
        // ...
        return { data: deletedRecord };
    },
    getListDeleted: (params) => {
        const { filter, sort, pagination } = params;
        // ...
        return { data: deletedRecords, total: deletedRecords.length };
    },

    restoreOne: (params) => {
        const { id } = params;
        // ...
        return { data: deletedRecord };
    },
    restoreMany: (params) => {
        const { ids } = params;
        // ...
        return { data: deletedRecords };
    },

    hardDelete: (params) => {
        const { id } = params;
        // ...
        return { data: deletedRecordId };
    },
    hardDeleteMany: (params) => {
        const { ids } = params;
        // ...
        return { data: deletedRecordsIds };
    },
};

Tip: ra-soft-delete will automatically populate the authorId using your authProvider's getIdentity method if there is one. It will use the id field of the returned identity object. Otherwise this field will be left blank.

Tip: Deleted records are immutable, so you don't need to implement an updateDeleted method.

A deleted record is an object with the following properties:

{
    id: 123,
    resource: "products",
    deleted_at: "2025-06-06T15:32:22Z",
    deleted_by: "johndoe",
    data: {
        id: 456,
        title: "Lorem ipsum",
        teaser: "Lorem ipsum dolor sit amet",
        body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
    },
}

ra-soft-delete comes with two built-in implementations that will add soft delete capabilities to your data provider:

  • addSoftDeleteBasedOnResource stores the deleted records for all resources in a single deleted_records (configurable) resource.
  • addSoftDeleteInPlace keeps the deleted records in the same resource, but fills deleted_at (configurable) and deleted_by (configurable) fields.

You can also write your own implementation. Feel free to look at these builders source code for inspiration. You can find it under your node_modules folder, e.g. at node_modules/@react-admin/ra-soft-delete/src/dataProvider/addSoftDeleteBasedOnResource.ts.

Once your provider has all soft-delete methods, pass it to the <Admin> component and you're ready to start using ra-soft-delete.

// in src/App.tsx
import { Admin } from 'react-admin';
import { dataProvider } from './dataProvider';

const App = () => <Admin dataProvider={dataProvider}>{/* ... */}</Admin>;
// in src/App.tsx
import { Admin } from "react-admin";
import { dataProvider } from "./dataProvider";

const App = () => <Admin dataProvider={dataProvider}>{/* ... */}</Admin>;

Each data provider verb has its own hook so you can use them in custom components:

addSoftDeleteBasedOnResource

Use addSoftDeleteBasedOnResource to add the soft delete capabilities to your data provider, storing all deleted records in a single deleted_records (configurable) resource.

// in src/dataProvider.ts
import { addSoftDeleteBasedOnResource } from '@react-admin/ra-soft-delete';
import baseDataProvider from './baseDataProvider';

export const dataProvider = addSoftDeleteBasedOnResource(
    baseDataProvider,
    { deletedRecordsResourceName: 'deleted_records' }
);
// in src/dataProvider.ts
import { addSoftDeleteBasedOnResource } from "@react-admin/ra-soft-delete";
import baseDataProvider from "./baseDataProvider";

export const dataProvider = addSoftDeleteBasedOnResource(baseDataProvider, {
    deletedRecordsResourceName: "deleted_records",
});

addSoftDeleteInPlace

Use addSoftDeleteInPlace to add the soft delete capabilities to your data provider, keeping the deleted records in the same resource. This implementation will simply fill the deleted_at (configurable) and deleted_by (configurable) fields.

You'll need to pass an object with all your resources as key so that getListDeleted knows where to look for deleted records.

Note on performances: Avoid calling getListDeleted without a resource filter, as it uses a naive implementation combining multiple getList calls, which can lead to bad performances. It is recommended to use one list per resource in this case (see <DeletedRecordsList resource> property).

// in src/dataProvider.ts
import { addSoftDeleteInPlace } from '@react-admin/ra-soft-delete';
import baseDataProvider from './baseDataProvider';

export const dataProvider = addSoftDeleteInPlace(
    baseDataProvider,
    {
        posts: {},
        comments: {
            deletedAtFieldName: 'deletion_date',
        },
        accounts: {
            deletedAtFieldName: 'disabled_at',
            deletedByFieldName: 'disabled_by',
        }
    }
);
// in src/dataProvider.ts
import { addSoftDeleteInPlace } from "@react-admin/ra-soft-delete";
import baseDataProvider from "./baseDataProvider";

export const dataProvider = addSoftDeleteInPlace(baseDataProvider, {
    posts: {},
    comments: {
        deletedAtFieldName: "deletion_date",
    },
    accounts: {
        deletedAtFieldName: "disabled_at",
        deletedByFieldName: "disabled_by",
    },
});

createMany

ra-soft-delete provides a default implementation of the createMany method that simply calls create multiple times. However, some data providers may be able to create multiple records at once, which can greatly improve performances.

const dataProviderWithCreateMany = {
    ...dataProvider,
    createMany: (resource, params: CreateManyParams): CreateManyResult => {
        const {data} = params; // data is an array of records.
        // ...
        return {data: createdRecords};
    },
};
const dataProviderWithCreateMany = {
    ...dataProvider,
    createMany: (resource, params) => {
        const { data } = params; // data is an array of records.
        // ...
        return { data: createdRecords };
    },
};

Usage

ra-soft-delete provides SoftDeleteButton and BulkSoftDeleteButton that can be used in place of the classic DeleteButton or BulkDeleteButton.

// in src/posts/PostEdit.ts
import * as React from "react";
import {
    Toolbar,
    SaveButton,
    Edit,
    SimpleForm,
} from 'react-admin';
import { SoftDeleteButton } from '@react-admin/ra-soft-delete';

const CustomToolbar = () => (
    <Toolbar sx={{ display: 'flex', justifyContent: 'space-between' }}>
        <SaveButton />
        <SoftDeleteButton />
    </Toolbar>
);

const PostEdit = () => (
    <Edit>
        <SimpleForm toolbar={<CustomToolbar />}>
            ...
        </SimpleForm>
    </Edit>
);
// in src/posts/PostEdit.ts
import * as React from "react";
import { Toolbar, SaveButton, Edit, SimpleForm } from "react-admin";
import { SoftDeleteButton } from "@react-admin/ra-soft-delete";

const CustomToolbar = () => (
    <Toolbar sx={{ display: "flex", justifyContent: "space-between" }}>
        <SaveButton />
        <SoftDeleteButton />
    </Toolbar>
);

const PostEdit = () => (
    <Edit>
        <SimpleForm toolbar={<CustomToolbar />}>...</SimpleForm>
    </Edit>
);

You can also use the buttons in a DataTable:

import { List, DataTable, BulkExportButton } from 'react-admin';
import { SoftDeleteButton, BulkSoftDeleteButton } from '@react-admin/ra-soft-delete';

const PostBulkActionButtons = () => (
    <>
        <BulkExportButton />
        <BulkSoftDeleteButton />
    </>
);

export const PostList = () => (
    <List>
        <DataTable bulkActionButtons={<PostBulkActionButtons />}>
            <DataTable.Col source="title" />
            <DataTable.Col label="Author">
                <ReferenceField source="author" reference="users" />
            </DataTable.Col>
            <DataTable.Col source="published_at" field={DateField} />
            <DataTable.Col 
                label="Summary"
                render={record => record.summary.substr(0, 10) + '...'} 
            />
            <DataTable.Col>
                <SoftDeleteButton />
            </DataTable.Col>
        </DataTable>
    </List>
);
import { List, DataTable, BulkExportButton } from "react-admin";
import { SoftDeleteButton, BulkSoftDeleteButton } from "@react-admin/ra-soft-delete";

const PostBulkActionButtons = () => (
    <>
        <BulkExportButton />
        <BulkSoftDeleteButton />
    </>
);

export const PostList = () => (
    <List>
        <DataTable bulkActionButtons={<PostBulkActionButtons />}>
            <DataTable.Col source="title" />
            <DataTable.Col label="Author">
                <ReferenceField source="author" reference="users" />
            </DataTable.Col>
            <DataTable.Col source="published_at" field={DateField} />
            <DataTable.Col label="Summary" render={(record) => record.summary.substr(0, 10) + "..."} />
            <DataTable.Col>
                <SoftDeleteButton />
            </DataTable.Col>
        </DataTable>
    </List>
);

When a record has been deleted, you can find it in the deleted records list. The <DeletedRecordsList> is a special component which can be added using a custom route:

// in src/App.js
import { Admin, Resource, CustomRoutes } from 'react-admin';
import { Route } from 'react-router-dom';
import { DeletedRecordsList } from '@react-admin/ra-soft-delete';

import { dataProvider } from './dataProvider';
import posts from './posts';
import comments from './comments';

export const App = () => (
    <Admin dataProvider={dataProvider}>
        <Resource name="posts" {...posts} />
        <Resource name="comments" {...comments} />
        <CustomRoutes>
            <Route path="/deleted" element={<DeletedRecordsList />} />
        </CustomRoutes>
    </Admin>
);
// in src/App.js
import { Admin, Resource, CustomRoutes } from "react-admin";
import { Route } from "react-router-dom";
import { DeletedRecordsList } from "@react-admin/ra-soft-delete";

import { dataProvider } from "./dataProvider";
import posts from "./posts";
import comments from "./comments";

export const App = () => (
    <Admin dataProvider={dataProvider}>
        <Resource name="posts" {...posts} />
        <Resource name="comments" {...comments} />
        <CustomRoutes>
            <Route path="/deleted" element={<DeletedRecordsList />} />
        </CustomRoutes>
    </Admin>
);

A deleted records list with deleted posts and comments

To add this custom route to the menu (see Adding Custom Routes to the Menu), ra-soft-delete provides a deleted records list menu item. This menu item links to /deleted by default, but you can configure it with the to property if you set a different route.

// in src/MyMenu.js
import { Menu } from 'react-admin';
import { DeletedRecordsListMenuItem } from '@react-admin/ra-soft-delete';

export const MyMenu = () => (
    <Menu>
        <Menu.DashboardItem />
        <Menu.ResourceItems />
        <DeletedRecordsListMenuItem />
    </Menu>
);
// in src/MyMenu.js
import { Menu } from "react-admin";
import { DeletedRecordsListMenuItem } from "@react-admin/ra-soft-delete";

export const MyMenu = () => (
    <Menu>
        <Menu.DashboardItem />
        <Menu.ResourceItems />
        <DeletedRecordsListMenuItem />
    </Menu>
);

Check the documentation of the available components to learn more.

I18N

This module uses specific translations for new buttons, notifications and confirm dialogs. Here is how to set up the i18n provider to use the default translations.

import { Admin, mergeTranslations } from 'react-admin';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';
import {
    raSoftDeleteLanguageEnglish,
    raSoftDeleteLanguageFrench,
} from '@react-admin/ra-soft-delete';

const i18nProvider = polyglotI18nProvider(
    locale =>
        locale === 'fr'
            ? mergeTranslations(frenchMessages, raSoftDeleteLanguageFrench)
            : mergeTranslations(englishMessages, raSoftDeleteLanguageEnglish),
    'en',
    [
        { locale: 'en', name: 'English' },
        { locale: 'fr', name: 'Français' },
    ]
);

export const MyApp = () => (
    <Admin i18nProvider={i18nProvider}>
        ...
    </Admin>
);
import { Admin, mergeTranslations } from "react-admin";
import polyglotI18nProvider from "ra-i18n-polyglot";
import englishMessages from "ra-language-english";
import frenchMessages from "ra-language-french";
import { raSoftDeleteLanguageEnglish, raSoftDeleteLanguageFrench } from "@react-admin/ra-soft-delete";

const i18nProvider = polyglotI18nProvider(
    (locale) =>
        locale === "fr"
            ? mergeTranslations(frenchMessages, raSoftDeleteLanguageFrench)
            : mergeTranslations(englishMessages, raSoftDeleteLanguageEnglish),
    "en",
    [
        { locale: "en", name: "English" },
        { locale: "fr", name: "Français" },
    ]
);

export const MyApp = () => <Admin i18nProvider={i18nProvider}>...</Admin>;

As for all translations in react-admin, it's possible to customize the messages. To create your own translations, you can use the TypeScript types to see the structure and see which keys are overridable. Here is an example of how to customize translations in your app:

import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';
import {
    raSoftDeleteLanguageEnglish,
    raSoftDeleteLanguageFrench,
    type RaSoftDeleteTranslationMessages,
} from '@react-admin/ra-soft-delete';
import type { TranslationMessages as BaseTranslationMessages } from 'react-admin';

/* TranslationMessages extends the defaut translation
 * Type from react-admin (BaseTranslationMessages)
 * and the ra-soft-delete translation Type (RaSoftDeleteTranslationMessages)
 */
interface TranslationMessages
    extends RaSoftDeleteTranslationMessages,
        BaseTranslationMessages {}

const customEnglishMessages: TranslationMessages = mergeTranslations(
    englishMessages,
    raSoftDeleteLanguageEnglish,
    {
        'ra-soft-delete': {
            action: {
                soft_delete: 'Archive element',
            },
            notification: {
                soft_deleted: 'Element archived |||| %{smart_count} elements archived',
                deleted_permanently: 'Element deleted permanently |||| %{smart_count} elements deleted permanently',
            },
        },
    }
);

const i18nCustomProvider = polyglotI18nProvider(locale => {
    if (locale === 'fr') {
        return mergeTranslations(frenchMessages, raSoftDeleteLanguageFrench);
    }
    return customEnglishMessages;
}, 'en');

export const MyApp = () => (
    <Admin dataProvider={myDataprovider} i18nProvider={i18nCustomProvider}>
        ...
    </Admin>
);
import polyglotI18nProvider from "ra-i18n-polyglot";
import englishMessages from "ra-language-english";
import frenchMessages from "ra-language-french";
import { raSoftDeleteLanguageEnglish, raSoftDeleteLanguageFrench } from "@react-admin/ra-soft-delete";

const customEnglishMessages = mergeTranslations(englishMessages, raSoftDeleteLanguageEnglish, {
    "ra-soft-delete": {
        action: {
            soft_delete: "Archive element",
        },
        notification: {
            soft_deleted: "Element archived |||| %{smart_count} elements archived",
            deleted_permanently: "Element deleted permanently |||| %{smart_count} elements deleted permanently",
        },
    },
});

const i18nCustomProvider = polyglotI18nProvider((locale) => {
    if (locale === "fr") {
        return mergeTranslations(frenchMessages, raSoftDeleteLanguageFrench);
    }
    return customEnglishMessages;
}, "en");

export const MyApp = () => (
    <Admin dataProvider={myDataprovider} i18nProvider={i18nCustomProvider}>
        ...
    </Admin>
);

<SoftDeleteButton>

Soft-deletes the current record.

A soft delete button in a <DataTable>

Usage

<SoftDeleteButton> reads the current record from RecordContext, and the current resource from ResourceContext, so in general it doesn't need any property:

import { SoftDeleteButton } from '@react-admin/ra-soft-delete';

const CommentShow = () => (
    <>
        {/* ... */}
        <SoftDeleteButton />
    </>
);
import { SoftDeleteButton } from "@react-admin/ra-soft-delete";

const CommentShow = () => (
    <>
        {/* ... */}
        <SoftDeleteButton />
    </>
);

When pressed, it will call dataProvider.softDelete() with the current record's id.

You can also call it with a record and a resource:

<SoftDeleteButton record={{ id: 123, author: 'John Doe' }} resource="comments" />
<SoftDeleteButton record={{ id: 123, author: "John Doe" }} resource="comments" />;

Props

Prop Required Type Default Description
className Optional string - Class name to customize the look and feel of the button element itself
label Optional string - label or translation message to use
icon Optional ReactElement <DeleteIcon> iconElement, e.g. <CommentIcon />
mutationMode Optional string 'undoable' Mutation mode ('undoable', 'pessimistic' or 'optimistic')
mutation Options Optional null options for react-query useMutation hook
record Optional Object - Record to soft delete, e.g. { id: 12, foo: 'bar' }
redirect Optional string, false or function 'list' Custom redirection after success side effect
resource Optional string - Resource to soft delete, e.g. 'posts'
sx Optional SxProps - The custom styling for the button
success Message Optional string 'Element deleted' Lets you customize the success notification message.

label

By default, the label is Archive in English. In other languages, it's the translation of the 'ra-soft-delete.action.soft_delete' key.

You can customize this label by providing a resource specific translation with the key resources.RESOURCE.action.soft_delete (e.g. resources.posts.action.soft_delete):

// in src/i18n/en.ts
import englishMessages from 'ra-language-english';

export const en = {
    ...englishMessages,
    resources: {
        posts: {
            name: 'Post |||| Posts',
            action: {
                soft_delete: 'Archive %{recordRepresentation}'
            }
        },
    },
    // ...
};
// in src/i18n/en.ts
import englishMessages from "ra-language-english";

export const en = {
    ...englishMessages,
    resources: {
        posts: {
            name: "Post |||| Posts",
            action: {
                soft_delete: "Archive %{recordRepresentation}",
            },
        },
    },
    // ...
};

You can also customize this label by specifying a custom label prop:

<SoftDeleteButton label="Delete this comment" />
<SoftDeleteButton label="Delete this comment" />;

Custom labels are automatically translated, so you can use a translation key, too:

<SoftDeleteButton label="resources.comments.actions.soft_delete" />
<SoftDeleteButton label="resources.comments.actions.soft_delete" />;

icon

Customize the icon of the button by passing an icon prop:

import AutoDeleteIcon from '@mui/icons-material/AutoDelete';

<SoftDeleteButton icon={<AutoDeleteIcon />} />

mutationMode

<SoftDeleteButton> has three modes, depending on the mutationMode prop:

  • 'undoable' (default): Clicking the button will update the UI optimistically and display a confirmation snackbar with an undo button. If the user clicks the undo button, the record will not be soft-deleted and the UI will be rolled back. Otherwise, the record will be soft-deleted after 5 seconds.
  • optimistic: Clicking the button will update the UI optimistically and soft-delete the record. If the soft-deletion fails, the UI will be rolled back.
  • pessimistic: Clicking the button will display a confirmation dialog. If the user confirms, the record will be soft-deleted. If the user cancels, nothing will happen.

Note: When choosing the pessimistic mode, <SoftDeleteButton> will actually render a <SoftDeleteWithConfirmButton> component and accept additional props to customize the confirm dialog (see below).

mutationOptions

<SoftDeleteButton> calls the useMutation hook internally to soft-delete the record. You can pass options to this hook using the mutationOptions prop.

<SoftDeleteButton mutationOptions={{ onError: () => alert('Record not deleted, please retry') }} />
<SoftDeleteButton mutationOptions={{ onError: () => alert("Record not deleted, please retry") }} />;

Check out the useMutation documentation for more information on the available options.

record

By default, <SoftDeleteButton> reads the current record from the RecordContext. If you want to delete a different record, you can pass it as a prop:

<SoftDeleteButton record={{ id: 123, author: 'John Doe' }} />
<SoftDeleteButton record={{ id: 123, author: "John Doe" }} />;

redirect

By default, <SoftDeleteButton> redirects to the list page after a successful deletion. You can customize the redirection by passing a path as the redirect prop:

<SoftDeleteButton redirect="/comments" />
<SoftDeleteButton redirect="/comments" />;

resource

By default, <SoftDeleteButton> reads the current resource from the ResourceContext. If you want to delete a record from a different resource, you can pass it as a prop:

<SoftDeleteButton record={{ id: 123, author: 'John Doe' }} resource="comments" />
<SoftDeleteButton record={{ id: 123, author: "John Doe" }} resource="comments" />;

successMessage

On success, <SoftDeleteButton> displays a "Element deleted" notification in English. <SoftDeleteButton> uses two successive translation keys to build the success message:

  • resources.{resource}.notifications.soft_deleted as a first choice
  • ra-soft-delete.notification.soft_deleted as a fallback

To customize the notification message, you can set custom translation for these keys in your i18nProvider.

Tip: If you choose to use a custom translation, be aware that react-admin uses the same translation message for the <SoftDeleteButton> and <BulkSoftDeleteButton>, so the message must support pluralization:

const englishMessages = {
    resources: {
        comments: {
            notifications: {
                soft_deleted: 'Comment archived |||| %{smart_count} comments archived',
                // ...
            },
        },
    },
};
const englishMessages = {
    resources: {
        comments: {
            notifications: {
                soft_deleted: "Comment archived |||| %{smart_count} comments archived",
                // ...
            },
        },
    },
};

Alternately, pass a successMessage prop:

<SoftDeleteButton successMessage="Comment deleted successfully" />
<SoftDeleteButton successMessage="Comment deleted successfully" />;

Access Control

If your authProvider implements Access Control, <SoftDeleteButton> will only render if the user has the "soft_delete" access to the related resource.

<SoftDeleteButton> will call authProvider.canAccess() using the following parameters:

{ action: "soft_delete", resource: [current resource], record: [current record] }

<SoftDeleteWithConfirmButton>

Soft-deletes the current record after a confirm dialog has been accepted.

Prop Required Type Default Description
className Optional string - Class name to customize the look and feel of the button element itself
confirmTitle Optional ReactNode 'ra-soft-delete.message.soft_delete_title' Title of the confirm dialog
confirmContent Optional ReactNode 'ra-soft-delete.message.soft_delete_content' Message or React component to be used as the body of the confirm dialog
confirmColor Optional 'primary' | 'warning' 'primary' The color of the confirm dialog's "Confirm" button
contentTranslateOptions Optional Object {} Custom id, name and record representation to be used in the confirm dialog's content
icon Optional ReactElement <DeleteIcon> iconElement, e.g. <CommentIcon />
label Optional string 'ra-soft-delete.action.soft_delete' label or translation message to use
mutationOptions Optional null options for react-query useMutation hook
redirect Optional string | false | Function 'list' Custom redirection after success side effect
titleTranslateOptions Optional Object {} Custom id, name and record representation to be used in the confirm dialog's title
successMessage Optional string 'ra-soft-delete.notification.soft_deleted' Lets you customize the success notification message.
import * as React from 'react';
import { Toolbar, Edit, SaveButton, useRecordContext } from 'react-admin';
import { SoftDeleteWithConfirmButton } from '@react-admin/ra-soft-delete';

const EditToolbar = () => {
    const record = useRecordContext();

    return (
        <Toolbar>
            <SaveButton/>
            <SoftDeleteWithConfirmButton
                confirmContent="You will be able to recover this record from the trash."
                confirmColor="warning"
                contentTranslateOptions={{ name: record.name }}
                titleTranslateOptions={{ name: record.name }}
            />
        </Toolbar>
    );
};

const MyEdit = () => (
    <Edit>
        <SimpleForm toolbar={<EditToolbar />}>
            ...
        </SimpleForm>        
    </Edit>    
);
import * as React from "react";
import { Toolbar, Edit, SaveButton, useRecordContext } from "react-admin";
import { SoftDeleteWithConfirmButton } from "@react-admin/ra-soft-delete";

const EditToolbar = () => {
    const record = useRecordContext();

    return (
        <Toolbar>
            <SaveButton />
            <SoftDeleteWithConfirmButton
                confirmContent="You will be able to recover this record from the trash."
                confirmColor="warning"
                contentTranslateOptions={{ name: record.name }}
                titleTranslateOptions={{ name: record.name }}
            />
        </Toolbar>
    );
};

const MyEdit = () => (
    <Edit>
        <SimpleForm toolbar={<EditToolbar />}>...</SimpleForm>
    </Edit>
);

<BulkSoftDeleteButton>

Soft-deletes the selected rows. To be used inside the <DataTable bulkActionButtons> prop.

A bulk soft delete button in a <DataTable>

Usage

<BulkSoftDeleteButton> reads the selected record ids from the ListContext, and the current resource from ResourceContext, so in general it doesn’t need any props:

import * as React from 'react';
import { BulkExportButton, DataTable } from 'react-admin';
import { BulkSoftDeleteButton } from '@react-admin/ra-soft-delete';

const PostBulkActionButtons = () => (
    <>
        <BulkExportButton />
        <BulkSoftDeleteButton />
    </>
);

export const PostList = () => (
    <List>
        <DataTable bulkActionButtons={<PostBulkActionButtons />}>
            ...
        </DataTable>
    </List>
);
import * as React from "react";
import { BulkExportButton, DataTable } from "react-admin";
import { BulkSoftDeleteButton } from "@react-admin/ra-soft-delete";

const PostBulkActionButtons = () => (
    <>
        <BulkExportButton />
        <BulkSoftDeleteButton />
    </>
);

export const PostList = () => (
    <List>
        <DataTable bulkActionButtons={<PostBulkActionButtons />}>...</DataTable>
    </List>
);

Props

Prop Required Type Default Description
confirmContent Optional React node - Lets you customize the content of the confirm dialog. Only used in 'pessimistic' or 'optimistic' mutation modes
confirmTitle Optional string - Lets you customize the title of the confirm dialog. Only used in 'pessimistic' or 'optimistic' mutation modes
confirmColor Optional 'primary' | 'warning' 'primary' Lets you customize the color of the confirm dialog's "Confirm" button. Only used in 'pessimistic' or 'optimistic' mutation modes
label Optional string 'ra-soft-delete.action.soft_delete' label or translation message to use
icon Optional ReactElement <DeleteIcon> iconElement, e.g. <CommentIcon />
mutationMode Optional string 'undoable' Mutation mode ('undoable', 'pessimistic' or 'optimistic')
mutationOptions Optional object null options for react-query useMutation hook
successMessage Optional string 'ra-soft-delete.notification.soft_deleted' Lets you customize the success notification message.

Tip: If you choose the 'pessimistic' or 'optimistic' mutation mode, a confirm dialog will be displayed to the user before the mutation is executed.

successMessage

On success, <BulkSoftDeleteButton> displays a "XX elements deleted" notification in English. <BulkSoftDeleteButton> uses two successive translation keys to build the success message:

  • resources.{resource}.notifications.soft_deleted as a first choice
  • ra-soft-delete.notification.soft_deleted as a fallback

To customize the notification message, you can set custom translation for these keys in your i18nProvider.

Tip: If you choose to use a custom translation, be aware that react-admin uses the same translation message for the <SoftDeleteButton>, so the message must support pluralization:

const englishMessages = {
    resources: {
        posts: {
            notifications: {
                soft_deleted: 'Post archived |||| %{smart_count} posts archived',
                // ...
            },
        },
    },
};
const englishMessages = {
    resources: {
        posts: {
            notifications: {
                soft_deleted: "Post archived |||| %{smart_count} posts archived",
                // ...
            },
        },
    },
};

Alternately, pass a successMessage prop:

<BulkSoftDeleteButton successMessage="Posts deleted successfully" />
<BulkSoftDeleteButton successMessage="Posts deleted successfully" />;

<DeletedRecordsList>

The <DeletedRecordsList> component fetches a list of deleted records from the data provider and display them in a <DataTable> with pagination, filters and sort.

The rendered <DataTable> includes buttons for restoring or permanently deleting the deleted records, and allows to show the deleted record data in a dialog when clicking on a row.

A deleted records list

Usage

<DeletedRecordsList> uses dataProvider.getListDeleted() to get the deleted records to display, so in general it doesn't need any property. However, you need to define the route to reach this component manually using <CustomRoutes>.

// in src/App.js
import { Admin, CustomRoutes } from 'react-admin';
import { Route } from 'react-router-dom';
import { DeletedRecordsList } from '@react-admin/ra-soft-delete';

export const App = () => (
    <Admin>
        ...
        <CustomRoutes>
            <Route path="/deleted" element={<DeletedRecordsList />} />
        </CustomRoutes>
    </Admin>
);
// in src/App.js
import { Admin, CustomRoutes } from "react-admin";
import { Route } from "react-router-dom";
import { DeletedRecordsList } from "@react-admin/ra-soft-delete";

export const App = () => (
    <Admin>
        ...
        <CustomRoutes>
            <Route path="/deleted" element={<DeletedRecordsList />} />
        </CustomRoutes>
    </Admin>
);

That's enough to display the deleted records list, with functional simple filters, sort and pagination.

Props

Prop Required Type Default Description
debounce Optional number 500 The debounce delay in milliseconds to apply when users change the sort or filter parameters.
children Optional Element <DeletedRecordsTable> The component used to render the list of deleted records.
detailComponents Optional Record<string, ComponentType> - The custom show components for each resource in the deleted records list.
disable Authentication Optional boolean false Set to true to disable the authentication check.
disable SyncWithLocation Optional boolean false Set to true to disable the synchronization of the list parameters with the URL.
filter Optional object - The permanent filter values.
filter DefaultValues Optional object - The default filter values.
mutation Mode Optional string 'undoable' Mutation mode ('undoable', 'pessimistic' or 'optimistic').
pagination Optional ReactElement <Pagination> The pagination component to use.
perPage Optional number 10 The number of records to fetch per page.
queryOptions Optional object - The options to pass to the useQuery hook.
resource Optional string - The resource of deleted records to fetch and display
sort Optional object { field: 'deleted_at', order: 'DESC' } The initial sort parameters.
storeKey Optional string or false - The key to use to store the current filter & sort. Pass false to disable store synchronization
title Optional `string ReactElement false`
sx Optional object - The CSS styles to apply to the component.

children

By default, <DeletedRecordsList> renders a <DeletedRecordsTable> component that displays the deleted records in a <DataTable>, with buttons to restore or permanently delete them. You can customize this table by passing custom children.

import { DataTable } from 'react-admin';
import { DeletedRecordsList } from '@react-admin/ra-soft-delete';

export const CustomDeletedRecords = () => (
    <DeletedRecordsList>
        <DataTable>
            <DataTable.Col source="id" />
            <DataTable.Col source="resource" />
            <DataTable.Col source="deleted_at" />
            <DataTable.Col source="deleted_by" />
            <DataTable.Col source="data.title" label="Title" />
        </DataTable>
    </DeletedRecordsList>
);
import { DataTable } from "react-admin";
import { DeletedRecordsList } from "@react-admin/ra-soft-delete";

export const CustomDeletedRecords = () => (
    <DeletedRecordsList>
        <DataTable>
            <DataTable.Col source="id" />
            <DataTable.Col source="resource" />
            <DataTable.Col source="deleted_at" />
            <DataTable.Col source="deleted_by" />
            <DataTable.Col source="data.title" label="Title" />
        </DataTable>
    </DeletedRecordsList>
);

debounce

By default, <DeletedRecordsList> does not refresh the data as soon as the user enters data in the filter form. Instead, it waits for half a second of user inactivity (via lodash.debounce) before calling the dataProvider on filter change. This is to prevent repeated (and useless) calls to the API.

You can customize the debounce duration in milliseconds - or disable it completely - by passing a debounce prop to the <DeletedRecordsList> component:

// wait 1 seconds instead of 500 milliseconds befoce calling the dataProvider
const DeletedRecordsWithDebounce = () => <DeletedRecordsList debounce={1000} />;
// wait 1 seconds instead of 500 milliseconds befoce calling the dataProvider
const DeletedRecordsWithDebounce = () => <DeletedRecordsList debounce={1000} />;

detailComponents

By default, <DeletedRecordsList> will show the deleted records data on click on a row of the <DataTable> in a <ShowGuesser>.

If you wish to customize the content in this show dialog, you can use the detailComponents prop to customize the dialog content for every resource in the list. The content is the same as a classic <Show> page.

However, you must use <ShowDeleted> component instead of <Show> to write a custom view for a deleted record. This is because <Show> gets a fresh version of the record from the data provider to display it, which is not possible in the deleted records list as the record is now deleted.

import { Admin, CustomRoutes, SimpleShowLayout, TextField } from 'react-admin';
import { Route } from 'react-router-dom';
import { DeletedRecordsList, ShowDeleted } from '@react-admin/ra-soft-delete';

const ShowDeletedBook = () => (
    <ShowDeleted>
        <SimpleShowLayout>
            <TextField source="title" />
            <TextField source="description" />
        </SimpleShowLayout>
    </ShowDeleted>
);

export const App = () => (
    <Admin>
        ...
        <CustomRoutes>
            <Route path="/deleted" element={
                <DeletedRecordsList detailComponents={{
                    books: ShowDeletedBook,
                }} />
            } />
        </CustomRoutes>
    </Admin>
);
import { Admin, CustomRoutes, SimpleShowLayout, TextField } from "react-admin";
import { Route } from "react-router-dom";
import { DeletedRecordsList, ShowDeleted } from "@react-admin/ra-soft-delete";

const ShowDeletedBook = () => (
    <ShowDeleted>
        <SimpleShowLayout>
            <TextField source="title" />
            <TextField source="description" />
        </SimpleShowLayout>
    </ShowDeleted>
);

export const App = () => (
    <Admin>
        ...
        <CustomRoutes>
            <Route
                path="/deleted"
                element={
                    <DeletedRecordsList
                        detailComponents={{
                            books: ShowDeletedBook,
                        }}
                    />
                }
            />
        </CustomRoutes>
    </Admin>
);

disableAuthentication

By default, <DeletedRecordsList> requires the user to be authenticated - any anonymous access redirects the user to the login page.

If you want to allow anonymous access to the deleted records list page, set the disableAuthentication prop to true.

const AnonymousDeletedRecords = () => <DeletedRecordsList disableAuthentication />;
const AnonymousDeletedRecords = () => <DeletedRecordsList disableAuthentication />;

disableSyncWithLocation

By default, react-admin synchronizes the <DeletedRecordsList> parameters (sort, pagination, filters) with the query string in the URL (using react-router location) and the Store.

You may want to disable this synchronization to keep the parameters in a local state, independent for each <DeletedRecordsList> instance. To do so, pass the disableSyncWithLocation prop. The drawback is that a hit on the "back" button doesn't restore the previous parameters.

const DeletedRecordsWithoutSyncWithLocation = () => <DeletedRecordsList disableSyncWithLocation />;
const DeletedRecordsWithoutSyncWithLocation = () => <DeletedRecordsList disableSyncWithLocation />;

Tip: disableSyncWithLocation also disables the persistence of the deleted records list parameters in the Store by default. To enable the persistence of the deleted records list parameters in the Store, you can pass a custom storeKey prop.

const DeletedRecordsSyncWithStore = () => <DeletedRecordsList disableSyncWithLocation storeKey="deletedRecordsListParams" />;
const DeletedRecordsSyncWithStore = () => (
    <DeletedRecordsList disableSyncWithLocation storeKey="deletedRecordsListParams" />
);

filter: Permanent Filter

You can choose to always filter the list, without letting the user disable this filter - for instance to display only published posts. Write the filter to be passed to the data provider in the filter props:

const DeletedPostsList = () => (
    <DeletedRecordsList filter={{ resource: 'posts' }} />
);
const DeletedPostsList = () => <DeletedRecordsList filter={{ resource: "posts" }} />;

The actual filter parameter sent to the data provider is the result of the combination of the user filters (the ones set through the filters component form), and the permanent filter. The user cannot override the permanent filters set by way of filter.

filterDefaultValues

To set default values to filters, you can pass an object literal as the filterDefaultValues prop of the <DeletedRecordsList> element.

const CustomDeletedRecords = () => (
    <DeletedRecordsList filterDefaultValues={{ resource: 'posts' }} />
);
const CustomDeletedRecords = () => <DeletedRecordsList filterDefaultValues={{ resource: "posts" }} />;

Tip: The filter and filterDefaultValues props have one key difference: the filterDefaultValues can be overridden by the user, while the filter values are always sent to the data provider. Or, to put it otherwise:

const filterSentToDataProvider = { ...filterDefaultValues, ...filterChosenByUser, ...filter };

mutationMode

The <DeletedRecordsList> list exposes restore and delete permanently buttons, which perform "mutations" (i.e. they alter the data). React-admin offers three modes for mutations. The mode determines when the side effects (redirection, notifications, etc.) are executed:

  • pessimistic: The mutation is passed to the dataProvider first. When the dataProvider returns successfully, the mutation is applied locally, and the side effects are executed.
  • optimistic: The mutation is applied locally and the side effects are executed immediately. Then the mutation is passed to the dataProvider. If the dataProvider returns successfully, nothing happens (as the mutation was already applied locally). If the dataProvider returns in error, the page is refreshed and an error notification is shown.
  • undoable (default): The mutation is applied locally and the side effects are executed immediately. Then a notification is shown with an undo button. If the user clicks on undo, the mutation is never sent to the dataProvider, and the page is refreshed. Otherwise, after a 5 seconds delay, the mutation is passed to the dataProvider. If the dataProvider returns successfully, nothing happens (as the mutation was already applied locally). If the dataProvider returns in error, the page is refreshed and an error notification is shown.

By default, <DeletedRecordsList> uses the undoable mutation mode. This is part of the "optimistic rendering" strategy of react-admin; it makes user interactions more reactive.

You can change this default by setting the mutationMode prop - and this affects all buttons in deleted records table. For instance, to remove the ability to undo the changes, use the optimistic mode:

const OptimisticDeletedRecords = () => (
    <DeletedRecordsList mutationMode="optimistic" />
);
const OptimisticDeletedRecords = () => <DeletedRecordsList mutationMode="optimistic" />;

And to make the actions blocking, and wait for the dataProvider response to continue, use the pessimistic mode:

const PessimisticDeletedRecords = () => (
    <DeletedRecordsList mutationMode="pessimistic" />
);
const PessimisticDeletedRecords = () => <DeletedRecordsList mutationMode="pessimistic" />;

Tip: When using any other mode than undoable, the <DeletePermanentlyButton> and <RestoreButton> display a confirmation dialog before calling the dataProvider.

pagination

By default, the <DeletedRecordsList> view displays a set of pagination controls at the bottom of the list.

The pagination prop allows to replace the default pagination controls by your own.

import { Pagination } from 'react-admin';
import { DeletedRecordsList } from '@react-admin/ra-soft-delete';

const DeletedRecordsPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100]} />;

export const DeletedRecordsWithCustomPagination = () => (
    <DeletedRecordsList pagination={<DeletedRecordsPagination />} />
);
import { Pagination } from "react-admin";
import { DeletedRecordsList } from "@react-admin/ra-soft-delete";

const DeletedRecordsPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100]} />;

export const DeletedRecordsWithCustomPagination = () => (
    <DeletedRecordsList pagination={<DeletedRecordsPagination />} />
);

See Paginating the List for details.

perPage

By default, the deleted records list paginates results by groups of 10. You can override this setting by specifying the perPage prop:

const DeletedRecordsWithCustomPagination = () => <DeletedRecordsList perPage={25} />;
const DeletedRecordsWithCustomPagination = () => <DeletedRecordsList perPage={25} />;

Note: The default pagination component's rowsPerPageOptions includes options of 5, 10, 25 and 50. If you set your deleted records list perPage to a value not in that set, you must also customize the pagination so that it allows this value, or else there will be an error.

const DeletedRecordsWithCustomPagination = () => (
-   <DeletedRecordsList perPage={6} />
+   <DeletedRecordsList perPage={6} pagination={<Pagination rowsPerPageOptions={[6, 12, 24, 36]} />} />
);

queryOptions

<DeletedRecordsList> accepts a queryOptions prop to pass query options to the react-query client. Check react-query's useQuery documentation for the list of available options.

This can be useful e.g. to pass a custom meta to the dataProvider.getListDeleted() call.

import { DeletedRecordsList } from '@react-admin/ra-soft-delete';

const CustomDeletedRecords = () => (
    <DeletedRecordsList queryOptions={{ meta: { foo: 'bar' } }} />
);
import { DeletedRecordsList } from "@react-admin/ra-soft-delete";

const CustomDeletedRecords = () => <DeletedRecordsList queryOptions={{ meta: { foo: "bar" } }} />;

With this option, react-admin will call dataProvider.getListDeleted() on mount with the meta: { foo: 'bar' } option.

You can also use the queryOptions prop to override the default error side effect. By default, when the dataProvider.getListDeleted() call fails, react-admin shows an error notification. Here is how to show a custom notification instead:

import { useNotify, useRedirect } from 'react-admin';
import { DeletedRecordsList } from '@react-admin/ra-soft-delete';

const CustomDeletedRecords = () => {
    const notify = useNotify();
    const redirect = useRedirect();

    const onError = (error) => {
        notify(`Could not load list: ${error.message}`, { type: 'error' });
        redirect('/dashboard');
    };

    return (
        <DeletedRecordsList queryOptions={{ onError }} />
    );
}
import { useNotify, useRedirect } from "react-admin";
import { DeletedRecordsList } from "@react-admin/ra-soft-delete";

const CustomDeletedRecords = () => {
    const notify = useNotify();
    const redirect = useRedirect();

    const onError = (error) => {
        notify(`Could not load list: ${error.message}`, { type: "error" });
        redirect("/dashboard");
    };

    return <DeletedRecordsList queryOptions={{ onError }} />;
};

The onError function receives the error from the dataProvider call (dataProvider.getListDeleted()), which is a JavaScript Error object (see the dataProvider documentation for details).

resource

<DeletedRecordsList> fetches the deleted records from the data provider using the dataProvider.getListDeleted() method. When no resource is specified, it will fetch all deleted records from all resources and display a filter.

If you want to display only the deleted records of a specific resource, you can pass the resource prop:

const DeletedPosts = () => (
    <DeletedRecordsList resource="posts" />
);
const DeletedPosts = () => <DeletedRecordsList resource="posts" />;

When a resource is specified, the filter will not be displayed, and the list will only show deleted records of that resource.

The title is also updated accordingly. Its translation key is ra-soft-delete.deleted_records_list.resource_title.

sort

Pass an object literal as the sort prop to determine the default field and order used for sorting:

const PessimisticDeletedRecords = () => (
    <DeletedRecordsList sort={{ field: 'id', order: 'ASC' }} />
);
const PessimisticDeletedRecords = () => <DeletedRecordsList sort={{ field: "id", order: "ASC" }} />;

sort defines the default sort order ; the list remains sortable by clicking on column headers.

For more details on list sort, see the Sorting The List section.

storeKey

By default, react-admin stores the list parameters (sort, pagination, filters) in localStorage so that users can come back to the list and find it in the same state as when they left it. The <DeletedRecordsList> component uses a specific identifier to store the list parameters under the key ra-soft-delete.listParams.

If you want to use multiple <DeletedRecordsList> and keep distinct store states for each of them (filters, sorting and pagination), you must give each list a unique storeKey property. You can also disable the persistence of list parameters and selection in the store by setting the storeKey prop to false.

In the example below, the deleted records lists store their list parameters separately (under the store keys 'deletedBooks' and 'deletedAuthors'). This allows to use both components in the same app, each having its own state (filters, sorting and pagination).

import { Admin, CustomRoutes } from 'react-admin';
import { Route } from 'react-router-dom';
import { DeletedRecordsList } from '@react-admin/ra-soft-delete';

const Admin = () => {
    return (
        <Admin dataProvider={dataProvider}>
            <CustomRoutes>
                <Route path="/books/deleted" element={
                    <DeletedRecordsList filter={{ resource: 'books' }} storeKey="deletedBooks" />
                } />
                <Route path="/authors/deleted" element={
                    <DeletedRecordsList filter={{ resource: 'authors' }} storeKey="deletedAuthors" />
                } />
            </CustomRoutes>
            <Resource name="books" />
        </Admin>
    );
};
import { CustomRoutes } from "react-admin";
import { Route } from "react-router-dom";
import { DeletedRecordsList } from "@react-admin/ra-soft-delete";

const Admin = () => {
    return (
        <Admin dataProvider={dataProvider}>
            <CustomRoutes>
                <Route
                    path="/books/deleted"
                    element={<DeletedRecordsList filter={{ resource: "books" }} storeKey="deletedBooks" />}
                />
                <Route
                    path="/authors/deleted"
                    element={<DeletedRecordsList filter={{ resource: "authors" }} storeKey="deletedAuthors" />}
                />
            </CustomRoutes>
            <Resource name="books" />
        </Admin>
    );
};

Tip: The storeKey is actually passed to the underlying useDeletedRecordsListController hook, which you can use directly for more complex scenarios. See the useDeletedRecordsListController doc for more info.

Note: Selection state will remain linked to a global key regardless of the specified storeKey string. This is a design choice because if row selection is not stored globally, then when a user permanently deletes or restores a record it may remain selected without any ability to unselect it. If you want to allow custom storeKey's for managing selection state, you will have to implement your own useDeletedRecordsListController hook and pass a custom key to the useRecordSelection hook. You will then need to implement your own delete buttons to manually unselect rows when deleting or restoring records. You can still opt out of all store interactions including selection if you set it to false.

title

The default title for a list view is the translation key ra-soft-delete.deleted_records_list.title.

You can also customize this title by specifying a custom title prop:

const DeletedRecordsWithTitle = () => <DeletedRecordsList title="Beautiful Trash" />;
const DeletedRecordsWithTitle = () => <DeletedRecordsList title="Beautiful Trash" />;

The title can be a string, a React element, or false to disable the title.

sx: CSS API

The <DeletedRecordsList> component accepts the usual className prop, but you can override many class names injected to the inner components by React-admin thanks to the sx property (see the sx documentation for syntax and examples). This property accepts the following subclasses:

Rule name Description
& .RaDeletedRecordsList-filters Applied to the filters container
& .RaDeletedRecordsList-table Applied to the <DataTable>
& .RaDeletedRecordsList-dialog Applied to the dialog shown when clicking on a row

For example:

const BeautifulDeletedRecordsList = () => (
    <DeletedRecordsList
        sx={{
            backgroundColor: 'yellow',
            '& .RaDeletedRecordsList-filters': {
                backgroundColor: 'red',
            },
        }}
    />
);
const BeautifulDeletedRecordsList = () => (
    <DeletedRecordsList
        sx={{
            backgroundColor: "yellow",
            "& .RaDeletedRecordsList-filters": {
                backgroundColor: "red",
            },
        }}
    />
);

Tip: The <DeletedRecordsList> component classes can also be customized for all instances of the component with its global css name RaDeletedRecordsList as described here.

<DeletedRecordsListMenuItem>

The <DeletedRecordsListMenuItem> component displays a menu item for the deleted records list.

// in src/MyMenu.tsx
import { Menu } from 'react-admin';
import { DeletedRecordsListMenuItem } from '@react-admin/ra-soft-delete';

export const MyMenu = () => (
    <Menu>
        <DeletedRecordsListMenuItem />
        ...
    </Menu>
);
// in src/MyMenu.tsx
import { Menu } from "react-admin";
import { DeletedRecordsListMenuItem } from "@react-admin/ra-soft-delete";

export const MyMenu = () => (
    <Menu>
        <DeletedRecordsListMenuItem />
        ...
    </Menu>
);

A deleted records list menu item

Clicking on the deleted records list menu item leads to the /deleted route by default. You can customize it using the to property:

// in src/MyMenu.tsx
import { Menu } from 'react-admin';
import { DeletedRecordsListMenuItem } from '@react-admin/ra-soft-delete';

export const MyMenu = () => (
    <Menu>
        <DeletedRecordsListMenuItem to="/trash" />
        ...
    </Menu>
);
// in src/MyMenu.tsx
import { Menu } from "react-admin";
import { DeletedRecordsListMenuItem } from "@react-admin/ra-soft-delete";

export const MyMenu = () => (
    <Menu>
        <DeletedRecordsListMenuItem to="/trash" />
        ...
    </Menu>
);

<DeletedRecordsListMenuItem> inherits all properties from <Menu.Item> component. This means that you can customize this menu item label by using the primaryText or children properties.

<ShowDeleted>

The <ShowDeleted> component replaces the <Show> component when displaying a deleted record.

It has the same properties as <Show>, apart from resource, id and queryOptions which are passed from the context and cannot be overridden. See <Show> props documentation for more info.

It is intended to be used with detailComponents of <DeletedRecordsList>.

import { Admin, CustomRoutes, SimpleShowLayout, TextField } from 'react-admin';
import { Route } from 'react-router-dom';
import { DeletedRecordsList, ShowDeleted } from '@react-admin/ra-soft-delete';

const ShowDeletedBook = () => (
    <ShowDeleted>
        <SimpleShowLayout>
            <TextField source="title" />
            <TextField source="description" />
        </SimpleShowLayout>
    </ShowDeleted>
);

export const App = () => (
    <Admin>
        ...
        <CustomRoutes>
            <Route path="/deleted" element={
                <DeletedRecordsList detailComponents={{
                    books: ShowDeletedBook,
                }} />
            } />
        </CustomRoutes>
    </Admin>
);
import { Admin, CustomRoutes, SimpleShowLayout, TextField } from "react-admin";
import { Route } from "react-router-dom";
import { DeletedRecordsList, ShowDeleted } from "@react-admin/ra-soft-delete";

const ShowDeletedBook = () => (
    <ShowDeleted>
        <SimpleShowLayout>
            <TextField source="title" />
            <TextField source="description" />
        </SimpleShowLayout>
    </ShowDeleted>
);

export const App = () => (
    <Admin>
        ...
        <CustomRoutes>
            <Route
                path="/deleted"
                element={
                    <DeletedRecordsList
                        detailComponents={{
                            books: ShowDeletedBook,
                        }}
                    />
                }
            />
        </CustomRoutes>
    </Admin>
);

It is rendered in a dialog opened on click on a row of the <DeletedRecordsTable>.

A dialog showing a deleted record

Access Control

If your authProvider implements Access Control, <DeletedRecordsList> will only be shown if the user has the "deleted_records" access on the virtual "ra-soft-delete" resource.

<DeletedRecordsList> will call authProvider.canAccess() using the following parameters:

{ action: "list_deleted_records", resource: "ra-soft-delete" }

Users without access will be redirected to the Access Denied page.

The permission action for the restore button is "restore", which means that authProvider.canAccess() will be called with the following parameters:

{ action: "restore", resource: "ra-soft-delete", record: [current record] }

Likewise, the permission action for the delete permanently button is "delete", which means that authProvider.canAccess() will be called with the following parameters:

{ action: "delete", resource: "ra-soft-delete", record: [current record] }

Hooks

useSoftDelete

This hook allows calling dataProvider.softDelete() when the callback is executed and deleting a single record based on its id.

const [softDeleteOne, { data, isPending, error }] = useSoftDelete(
    resource,
    { id, authorId, previousData, meta },
    options,
);
const [softDeleteOne, { data, isPending, error }] = useSoftDelete(
    resource,
    { id, authorId, previousData, meta },
    options
);

The softDeleteOne() method can be called with the same parameters as the hook:

const [softDeleteOne, { data, isPending, error }] = useSoftDelete();

// ...

softDeleteOne(
    resource,
    { id, authorId, previousData, meta },
    options,
);
const [softDeleteOne, { data, isPending, error }] = useSoftDelete();

// ...

softDeleteOne(resource, { id, authorId, previousData, meta }, options);

So, should you pass the parameters when calling the hook, or when executing the callback? It's up to you; but if you have the choice, we recommend passing the parameters when calling the softDeleteOne callback (second example).

Tip: If it's not provided, useSoftDelete will automatically populate the authorId using your authProvider's getIdentity method if there is one. It will use the id field of the returned identity object. Otherwise this field will be left blank.

Usage

// set params when calling the hook
import { useRecordContext } from 'react-admin';
import { useSoftDelete } from '@react-admin/ra-soft-delete';

const SoftDeleteButton = () => {
    const record = useRecordContext();
    const [softDeleteOne, { isPending, error }] = useSoftDelete(
        'likes',
        { id: record.id, previousData: record }
    );
    const handleClick = () => {
        softDeleteOne();
    }
    if (error) { return <p>ERROR</p>; }
    return <button disabled={isPending} onClick={handleClick}>Delete</button>;
};

// set params when calling the softDeleteOne callback
import { useRecordContext } from 'react-admin';
import { useSoftDelete } from '@react-admin/ra-soft-delete';

const SoftDeleteButton = () => {
    const record = useRecordContext();
    const [softDeleteOne, { isPending, error }] = useSoftDelete();
    const handleClick = () => {
        softDeleteOne(
            'likes',
            { id: record.id, previousData: record }
        );
    }
    if (error) { return <p>ERROR</p>; }
    return <button disabled={isPending} onClick={handleClick}>Delete</button>;
};
// set params when calling the hook
import { useRecordContext } from "react-admin";
import { useSoftDelete } from "@react-admin/ra-soft-delete";

const SoftDeleteButton = () => {
    const record = useRecordContext();
    const [softDeleteOne, { isPending, error }] = useSoftDelete("likes", { id: record.id, previousData: record });
    const handleClick = () => {
        softDeleteOne();
    };
    if (error) {
        return <p>ERROR</p>;
    }
    return (
        <button disabled={isPending} onClick={handleClick}>
            Delete
        </button>
    );
};

const SoftDeleteButton = () => {
    const record = useRecordContext();
    const [softDeleteOne, { isPending, error }] = useSoftDelete();
    const handleClick = () => {
        softDeleteOne("likes", { id: record.id, previousData: record });
    };
    if (error) {
        return <p>ERROR</p>;
    }
    return (
        <button disabled={isPending} onClick={handleClick}>
            Delete
        </button>
    );
};

TypeScript

The useSoftDelete hook accepts a generic parameter for the record type and another for the error type:

useSoftDelete<Product, Error>(undefined, undefined, {
    onError: (error) => {
        // TypeScript knows that error is of type Error
    },
    onSettled: (data, error) => {
        // TypeScript knows that data is of type Product
        // TypeScript knows that error is of type Error
    },
});
useSoftDelete(undefined, undefined, {
    onError: (error) => {
        // TypeScript knows that error is of type Error
    },
    onSettled: (data, error) => {
        // TypeScript knows that data is of type Product
        // TypeScript knows that error is of type Error
    },
});

useSoftDeleteMany

This hook allows calling dataProvider.softDeleteMany() when the callback is executed and deleting an array of records based on their ids.

const [softDeleteMany, { data, isPending, error }] = useSoftDeleteMany(
    resource,
    { ids, authorId, meta },
    options,
);
const [softDeleteMany, { data, isPending, error }] = useSoftDeleteMany(resource, { ids, authorId, meta }, options);

The softDeleteMany() method can be called with the same parameters as the hook:

const [softDeleteMany, { data, isPending, error }] = useSoftDeleteMany();

// ...

softDeleteMany(
    resource,
    { ids, authorId, meta },
    options,
);
const [softDeleteMany, { data, isPending, error }] = useSoftDeleteMany();

// ...

softDeleteMany(resource, { ids, authorId, meta }, options);

So, should you pass the parameters when calling the hook, or when executing the callback? It's up to you; but if you have the choice, we recommend passing the parameters when calling the softDeleteMany callback (second example).

Tip: If it's not provided, useSoftDeleteMany will automatically populate the authorId using your authProvider's getIdentity method if there is one. It will use the id field of the returned identity object. Otherwise this field will be left blank.

Usage

// set params when calling the hook
import { useListContext } from 'react-admin';
import { useSoftDeleteMany } from '@react-admin/ra-soft-delete';

const BulkSoftDeletePostsButton = () => {
    const { selectedIds } = useListContext();
    const [softDeleteMany, { isPending, error }] = useSoftDeleteMany(
        'posts',
        { ids: selectedIds }
    );
    const handleClick = () => {
        softDeleteMany();
    }
    if (error) { return <p>ERROR</p>; }
    return <button disabled={isPending} onClick={handleClick}>Delete selected posts</button>;
};

// set params when calling the softDeleteMany callback
import { useListContext } from 'react-admin';
import { useSoftDeleteMany } from '@react-admin/ra-soft-delete';

const BulkSoftDeletePostsButton = () => {
    const { selectedIds } = useListContext();
    const [softDeleteMany, { isPending, error }] = useSoftDeleteMany();
    const handleClick = () => {
        softDeleteMany(
            'posts',
            { ids: seletedIds }
        );
    }
    if (error) { return <p>ERROR</p>; }
    return <button disabled={isPending} onClick={handleClick}>Delete selected posts</button>;
};
// set params when calling the hook
import { useListContext } from "react-admin";
import { useSoftDeleteMany } from "@react-admin/ra-soft-delete";

const BulkSoftDeletePostsButton = () => {
    const { selectedIds } = useListContext();
    const [softDeleteMany, { isPending, error }] = useSoftDeleteMany("posts", { ids: selectedIds });
    const handleClick = () => {
        softDeleteMany();
    };
    if (error) {
        return <p>ERROR</p>;
    }
    return (
        <button disabled={isPending} onClick={handleClick}>
            Delete selected posts
        </button>
    );
};

const BulkSoftDeletePostsButton = () => {
    const { selectedIds } = useListContext();
    const [softDeleteMany, { isPending, error }] = useSoftDeleteMany();
    const handleClick = () => {
        softDeleteMany("posts", { ids: seletedIds });
    };
    if (error) {
        return <p>ERROR</p>;
    }
    return (
        <button disabled={isPending} onClick={handleClick}>
            Delete selected posts
        </button>
    );
};

TypeScript

The useSoftDeleteMany hook accepts a generic parameter for the record type and another for the error type:

useSoftDeleteMany<Product, Error>(undefined, undefined, {
    onError: (error) => {
        // TypeScript knows that error is of type Error
    },
    onSettled: (data, error) => {
        // TypeScript knows that data is of type Product[]
        // TypeScript knows that error is of type Error
    },
});
useSoftDeleteMany(undefined, undefined, {
    onError: (error) => {
        // TypeScript knows that error is of type Error
    },
    onSettled: (data, error) => {
        // TypeScript knows that data is of type Product[]
        // TypeScript knows that error is of type Error
    },
});

useGetListDeleted

This hook calls dataProvider.getListDeleted() when the component mounts. It's ideal for getting a list of deleted records. It supports filtering, sorting and pagination.

const { data, total, isPending, error, refetch, meta } = useGetListDeleted(
    {
        pagination: { page, perPage },
        sort: { field, order },
        filter,
        meta
    },
    options
);
const { data, total, isPending, error, refetch, meta } = useGetListDeleted(
    {
        pagination: { page, perPage },
        sort: { field, order },
        filter,
        meta,
    },
    options
);

The meta argument is optional. It can be anything you want to pass to the data provider, e.g. a list of fields to show in the result. It is distinct from the meta property of the response, which may contain additional metadata returned by the data provider.

The options parameter is optional, and is passed to react-query's useQuery hook. Check react-query's useQuery hook documentation for details on all available option.

The react-query query key for this hook is ['getListDeleted', { pagination, sort, filter, meta }].

Usage

Call the useGetListDeleted hook when you need to fetch a list of deleted records from the data provider.

import { useGetListDeleted } from '@react-admin/ra-soft-delete';

const LatestDeletedPosts = () => {
    const { data, total, isPending, error } = useGetListDeleted(
        { 
            filter: { resource: "posts" },
            pagination: { page: 1, perPage: 10 },
            sort: { field: 'deleted_at', order: 'DESC' }
        }
    );
    if (isPending) { return <Loading />; }
    if (error) { return <p>ERROR</p>; }
    return (
        <>
            <h1>Latest deleted posts</h1>
            <ul>
                {data.map(deletedRecord =>
                    <li key={deletedRecord.id}>{deletedRecord.data.title}</li>
                )}
            </ul>
            <p>{data.length} / {total} deleted posts</p>
        </>
    );
};
import { useGetListDeleted } from "@react-admin/ra-soft-delete";

const LatestDeletedPosts = () => {
    const { data, total, isPending, error } = useGetListDeleted({
        filter: { resource: "posts" },
        pagination: { page: 1, perPage: 10 },
        sort: { field: "deleted_at", order: "DESC" },
    });
    if (isPending) {
        return <Loading />;
    }
    if (error) {
        return <p>ERROR</p>;
    }
    return (
        <>
            <h1>Latest deleted posts</h1>
            <ul>
                {data.map((deletedRecord) => (
                    <li key={deletedRecord.id}>{deletedRecord.data.title}</li>
                ))}
            </ul>
            <p>
                {data.length} / {total} deleted posts
            </p>
        </>
    );
};

If you need to learn more about pagination, sort or filter, please refer to (useGetList documentation)(https://marmelab.com/react-admin/useGetList.html), as useGetListDeleted implements these parameters the same way.

TypeScript

The useGetListDeleted hook accepts a generic parameter for the record type:

import { useGetListDeleted } from '@react-admin/ra-soft-delete';

const LatestDeletedPosts = () => {
    const { data, total, isPending, error } = useGetListDeleted<Post>(
        { 
            filter: { resource: "posts" },
            pagination: { page: 1, perPage: 10 },
            sort: { field: 'deleted_at', order: 'DESC' }
        }
    );
    if (isPending) { return <Loading />; }
    if (error) { return <p>ERROR</p>; }
    return (
        <>
            <h1>Latest deleted posts</h1>
            <ul>
                {/* TypeScript knows that data is of type DeletedRecordType<Post>[] */}
                {data.map(deletedRecord =>
                    <li key={deletedRecord.id}>{deletedRecord.data.title}</li>
                )}
            </ul>
            <p>{data.length} / {total} deleted posts</p>
        </>
    );
};
import { useGetListDeleted } from "@react-admin/ra-soft-delete";

const LatestDeletedPosts = () => {
    const { data, total, isPending, error } = useGetListDeleted({
        filter: { resource: "posts" },
        pagination: { page: 1, perPage: 10 },
        sort: { field: "deleted_at", order: "DESC" },
    });
    if (isPending) {
        return <Loading />;
    }
    if (error) {
        return <p>ERROR</p>;
    }
    return (
        <>
            <h1>Latest deleted posts</h1>
            <ul>
                {/* TypeScript knows that data is of type DeletedRecordType<Post>[] */}
                {data.map((deletedRecord) => (
                    <li key={deletedRecord.id}>{deletedRecord.data.title}</li>
                ))}
            </ul>
            <p>
                {data.length} / {total} deleted posts
            </p>
        </>
    );
};

useGetOneDeleted

This hook calls dataProvider.getOneDeleted() when the component mounts. It queries the data provider for a single deleted record, based on its id.

const { data, isPending, error, refetch } = useGetOne(
    { id, meta },
    options
);
const { data, isPending, error, refetch } = useGetOne({ id, meta }, options);

The meta argument is optional. It can be anything you want to pass to the data provider, e.g. a list of fields to show in the result.

The options parameter is optional, and is passed to react-query's useQuery hook. Check react-query's useQuery hook documentation for details on all available option.

The react-query query key for this hook is ['getOneDeleted', { id: String(id), meta }].

Usage

Call useGetOneDeleted in a component to query the data provider for a single deleted record, based on its id.

import { useGetOneDeleted } from '@react-admin/ra-soft-delete';

const DeletedUser = ({ deletedUserId }) => {
    const { data: deletedUser, isPending, error } = useGetOneDeleted({ id: deletedUserId });
    if (isPending) { return <Loading />; }
    if (error) { return <p>ERROR</p>; }
    return <div>User {deletedUser.data.username} (deleted by {deletedUser.deleted_by})</div>;
};
import { useGetOneDeleted } from "@react-admin/ra-soft-delete";

const DeletedUser = ({ deletedUserId }) => {
    const { data: deletedUser, isPending, error } = useGetOneDeleted({ id: deletedUserId });
    if (isPending) {
        return <Loading />;
    }
    if (error) {
        return <p>ERROR</p>;
    }
    return (
        <div>
            User {deletedUser.data.username} (deleted by {deletedUser.deleted_by})
        </div>
    );
};

TypeScript

The useGetOneDeleted hook accepts a generic parameter for the record type:

import { useGetOneDeleted } from '@react-admin/ra-soft-delete';

const DeletedUser = ({ deletedUserId }) => {
    const { data: deletedUser, isPending, error } = useGetOneDeleted<User>({ id: deletedUserId });
    if (isPending) { return <Loading />; }
    if (error) { return <p>ERROR</p>; }
    // TypeScript knows that deletedUser.data is of type User
    return <div>User {deletedUser.data.username} (deleted by {deletedUser.deleted_by})</div>;
};
import { useGetOneDeleted } from "@react-admin/ra-soft-delete";

const DeletedUser = ({ deletedUserId }) => {
    const { data: deletedUser, isPending, error } = useGetOneDeleted({ id: deletedUserId });
    if (isPending) {
        return <Loading />;
    }
    if (error) {
        return <p>ERROR</p>;
    }
    // TypeScript knows that deletedUser.data is of type User
    return (
        <div>
            User {deletedUser.data.username} (deleted by {deletedUser.deleted_by})
        </div>
    );
};

useRestoreOne

This hook allows calling dataProvider.restoreOne() when the callback is executed and restoring a single deleted record based on its id.

Warning: The id here is the ID of the deleted record, and not the ID of the actual record that has been deleted.

const [restoreOne, { data, isPending, error }] = useRestoreOne(
    { id, meta },
    options,
);
const [restoreOne, { data, isPending, error }] = useRestoreOne({ id, meta }, options);

The restoreOne() method can be called with the same parameters as the hook:

const [restoreOne, { data, isPending, error }] = useRestoreOne();

// ...

restoreOne(
    { id, meta },
    options,
);
const [restoreOne, { data, isPending, error }] = useRestoreOne();

// ...

restoreOne({ id, meta }, options);

So, should you pass the parameters when calling the hook, or when executing the callback? It's up to you; but if you have the choice, we recommend passing the parameters when calling the restoreOne callback (second example).

Usage

// set params when calling the hook
import { useRecordContext } from 'react-admin';
import { useRestoreOne } from '@react-admin/ra-soft-delete';

const RestoreButton = () => {
    const deletedRecord = useRecordContext();
    const [restoreOne, { isPending, error }] = useRestoreOne(
        { id: deletedRecord.id }
    );
    const handleClick = () => {
        restoreOne();
    }
    if (error) { return <p>ERROR</p>; }
    return <button disabled={isPending} onClick={handleClick}>Restore</button>;
};

// set params when calling the restoreOne callback
import { useRecordContext } from 'react-admin';
import { useRestoreOne } from '@react-admin/ra-soft-delete';

const HardDeleteButton = () => {
    const deletedRecord = useRecordContext();
    const [restoreOne, { isPending, error }] = useRestoreOne();
    const handleClick = () => {
        restoreOne(
            { id: deletedRecord.id }
        );
    }
    if (error) { return <p>ERROR</p>; }
    return <button disabled={isPending} onClick={handleClick}>Restore</button>;
};
// set params when calling the hook
import { useRecordContext } from "react-admin";
import { useRestoreOne } from "@react-admin/ra-soft-delete";

const RestoreButton = () => {
    const deletedRecord = useRecordContext();
    const [restoreOne, { isPending, error }] = useRestoreOne({ id: deletedRecord.id });
    const handleClick = () => {
        restoreOne();
    };
    if (error) {
        return <p>ERROR</p>;
    }
    return (
        <button disabled={isPending} onClick={handleClick}>
            Restore
        </button>
    );
};

const HardDeleteButton = () => {
    const deletedRecord = useRecordContext();
    const [restoreOne, { isPending, error }] = useRestoreOne();
    const handleClick = () => {
        restoreOne({ id: deletedRecord.id });
    };
    if (error) {
        return <p>ERROR</p>;
    }
    return (
        <button disabled={isPending} onClick={handleClick}>
            Restore
        </button>
    );
};

TypeScript

The useRestoreOne hook accepts a generic parameter for the record type and another for the error type:

useRestoreOne<Product, Error>(undefined, undefined, {
    onError: (error) => {
        // TypeScript knows that error is of type Error
    },
    onSettled: (data, error) => {
        // TypeScript knows that data is of type DeletedRecordType<Product>
        // TypeScript knows that error is of type Error
    },
});
useRestoreOne(undefined, undefined, {
    onError: (error) => {
        // TypeScript knows that error is of type Error
    },
    onSettled: (data, error) => {
        // TypeScript knows that data is of type DeletedRecordType<Product>
        // TypeScript knows that error is of type Error
    },
});

useRestoreMany

This hook allows calling dataProvider.restoreMany() when the callback is executed and restoring an array of deleted records based on their ids.

Warning: The ids here are the IDs of the deleted records, and not the IDs of the actual records that have been deleted.

const [restoreMany, { data, isPending, error }] = useRestoreMany(
    { ids, meta },
    options,
);
const [restoreMany, { data, isPending, error }] = useRestoreMany({ ids, meta }, options);

The restoreMany() method can be called with the same parameters as the hook:

const [restoreMany, { data, isPending, error }] = useRestoreMany();

// ...

restoreMany(
    { ids, meta },
    options,
);
const [restoreMany, { data, isPending, error }] = useRestoreMany();

// ...

restoreMany({ ids, meta }, options);

So, should you pass the parameters when calling the hook, or when executing the callback? It's up to you; but if you have the choice, we recommend passing the parameters when calling the restoreMany callback (second example).

Usage

// set params when calling the hook
import { useListContext } from 'react-admin';
import { useRestoreMany } from '@react-admin/ra-soft-delete';

const BulkRestorePostsButton = () => {
    const { selectedIds } = useListContext();
    const [restoreMany, { isPending, error }] = useRestoreMany(
        { ids: selectedIds }
    );
    const handleClick = () => {
        restoreMany();
    }
    if (error) { return <p>ERROR</p>; }
    return <button disabled={isPending} onClick={handleClick}>Restore selected posts</button>;
};

// set params when calling the restoreMany callback
import { useListContext } from 'react-admin';
import { useRestoreMany } from '@react-admin/ra-soft-delete';

const BulkRestorePostsButton = () => {
    const { selectedIds } = useListContext();
    const [restoreMany, { isPending, error }] = useRestoreMany();
    const handleClick = () => {
        restoreMany(
            { ids: seletedIds }
        );
    }
    if (error) { return <p>ERROR</p>; }
    return <button disabled={isPending} onClick={handleClick}>Restore selected posts</button>;
};
// set params when calling the hook
import { useListContext } from "react-admin";
import { useRestoreMany } from "@react-admin/ra-soft-delete";

const BulkRestorePostsButton = () => {
    const { selectedIds } = useListContext();
    const [restoreMany, { isPending, error }] = useRestoreMany({ ids: selectedIds });
    const handleClick = () => {
        restoreMany();
    };
    if (error) {
        return <p>ERROR</p>;
    }
    return (
        <button disabled={isPending} onClick={handleClick}>
            Restore selected posts
        </button>
    );
};

const BulkRestorePostsButton = () => {
    const { selectedIds } = useListContext();
    const [restoreMany, { isPending, error }] = useRestoreMany();
    const handleClick = () => {
        restoreMany({ ids: seletedIds });
    };
    if (error) {
        return <p>ERROR</p>;
    }
    return (
        <button disabled={isPending} onClick={handleClick}>
            Restore selected posts
        </button>
    );
};

TypeScript

The useRestoreMany hook accepts a generic parameter for the record type and another for the error type:

useRestoreMany<Product, Error>(undefined, undefined, {
    onError: (error) => {
        // TypeScript knows that error is of type Error
    },
    onSettled: (data, error) => {
        // TypeScript knows that data is of type DeletedRecordType<Product>[]
        // TypeScript knows that error is of type Error
    },
});
useRestoreMany(undefined, undefined, {
    onError: (error) => {
        // TypeScript knows that error is of type Error
    },
    onSettled: (data, error) => {
        // TypeScript knows that data is of type DeletedRecordType<Product>[]
        // TypeScript knows that error is of type Error
    },
});

useHardDelete

This hook allows calling dataProvider.hardDelete() when the callback is executed and deleting a single deleted record based on its id.

Warning: The id here is the ID of the deleted record, and not the ID of the actual record that has been deleted.

const [hardDeleteOne, { data, isPending, error }] = useHardDelete(
    { id, previousData, meta },
    options,
);
const [hardDeleteOne, { data, isPending, error }] = useHardDelete({ id, previousData, meta }, options);

The hardDeleteOne() method can be called with the same parameters as the hook:

const [hardDeleteOne, { data, isPending, error }] = useHardDelete();

// ...

hardDeleteOne(
    { id, previousData, meta },
    options,
);
const [hardDeleteOne, { data, isPending, error }] = useHardDelete();

// ...

hardDeleteOne({ id, previousData, meta }, options);

So, should you pass the parameters when calling the hook, or when executing the callback? It's up to you; but if you have the choice, we recommend passing the parameters when calling the hardDeleteOne callback (second example).

Usage

// set params when calling the hook
import { useRecordContext } from 'react-admin';
import { useHardDelete } from '@react-admin/ra-soft-delete';

const HardDeleteButton = () => {
    const deletedRecord = useRecordContext();
    const [hardDeleteOne, { isPending, error }] = useHardDelete(
        { id: deletedRecord.id, previousData: record }
    );
    const handleClick = () => {
        hardDeleteOne();
    }
    if (error) { return <p>ERROR</p>; }
    return <button disabled={isPending} onClick={handleClick}>Delete</button>;
};

// set params when calling the hardDeleteOne callback
import { useRecordContext } from 'react-admin';
import { useHardDelete } from '@react-admin/ra-soft-delete';

const HardDeleteButton = () => {
    const deletedRecord = useRecordContext();
    const [hardDeleteOne, { isPending, error }] = useHardDelete();
    const handleClick = () => {
        hardDeleteOne(
            { id: deletedRecord.id, previousData: record }
        );
    }
    if (error) { return <p>ERROR</p>; }
    return <button disabled={isPending} onClick={handleClick}>Delete</button>;
};
// set params when calling the hook
import { useRecordContext } from "react-admin";
import { useHardDelete } from "@react-admin/ra-soft-delete";

const HardDeleteButton = () => {
    const deletedRecord = useRecordContext();
    const [hardDeleteOne, { isPending, error }] = useHardDelete({ id: deletedRecord.id, previousData: record });
    const handleClick = () => {
        hardDeleteOne();
    };
    if (error) {
        return <p>ERROR</p>;
    }
    return (
        <button disabled={isPending} onClick={handleClick}>
            Delete
        </button>
    );
};

const HardDeleteButton = () => {
    const deletedRecord = useRecordContext();
    const [hardDeleteOne, { isPending, error }] = useHardDelete();
    const handleClick = () => {
        hardDeleteOne({ id: deletedRecord.id, previousData: record });
    };
    if (error) {
        return <p>ERROR</p>;
    }
    return (
        <button disabled={isPending} onClick={handleClick}>
            Delete
        </button>
    );
};

TypeScript

The useHardDelete hook accepts a generic parameter for the record type and another for the error type:

useHardDelete<Product, Error>(undefined, undefined, {
    onError: (error) => {
        // TypeScript knows that error is of type Error
    },
    onSettled: (data, error) => {
        // TypeScript knows that data is of type DeletedRecordType<Product>
        // TypeScript knows that error is of type Error
    },
});
useHardDelete(undefined, undefined, {
    onError: (error) => {
        // TypeScript knows that error is of type Error
    },
    onSettled: (data, error) => {
        // TypeScript knows that data is of type DeletedRecordType<Product>
        // TypeScript knows that error is of type Error
    },
});

useHardDeleteMany

This hook allows calling dataProvider.hardDeleteMany() when the callback is executed and deleting an array of deleted records based on their ids.

Warning: The ids here are the IDs of the deleted records, and not the IDs of the actual records that have been deleted.

const [hardDeleteMany, { data, isPending, error }] = useHardDeleteMany(
    { ids, meta },
    options,
);
const [hardDeleteMany, { data, isPending, error }] = useHardDeleteMany({ ids, meta }, options);

The hardDeleteMany() method can be called with the same parameters as the hook:

const [hardDeleteMany, { data, isPending, error }] = useHardDeleteMany();

// ...

hardDeleteMany(
    { ids, meta },
    options,
);
const [hardDeleteMany, { data, isPending, error }] = useHardDeleteMany();

// ...

hardDeleteMany({ ids, meta }, options);

So, should you pass the parameters when calling the hook, or when executing the callback? It's up to you; but if you have the choice, we recommend passing the parameters when calling the hardDeleteMany callback (second example).

Usage

// set params when calling the hook
import { useListContext } from 'react-admin';
import { useHardDeleteMany } from '@react-admin/ra-soft-delete';

const BulkHardDeletePostsButton = () => {
    const { selectedIds } = useListContext();
    const [hardDeleteMany, { isPending, error }] = useHardDeleteMany(
        { ids: selectedIds }
    );
    const handleClick = () => {
        hardDeleteMany();
    }
    if (error) { return <p>ERROR</p>; }
    return <button disabled={isPending} onClick={handleClick}>Delete selected posts</button>;
};

// set params when calling the hardDeleteMany callback
import { useListContext } from 'react-admin';
import { useHardDeleteMany } from '@react-admin/ra-soft-delete';

const BulkHardDeletePostsButton = () => {
    const { selectedIds } = useListContext();
    const [hardDeleteMany, { isPending, error }] = useHardDeleteMany();
    const handleClick = () => {
        hardDeleteMany(
            { ids: seletedIds }
        );
    }
    if (error) { return <p>ERROR</p>; }
    return <button disabled={isPending} onClick={handleClick}>Delete selected posts</button>;
};
// set params when calling the hook
import { useListContext } from "react-admin";
import { useHardDeleteMany } from "@react-admin/ra-soft-delete";

const BulkHardDeletePostsButton = () => {
    const { selectedIds } = useListContext();
    const [hardDeleteMany, { isPending, error }] = useHardDeleteMany({ ids: selectedIds });
    const handleClick = () => {
        hardDeleteMany();
    };
    if (error) {
        return <p>ERROR</p>;
    }
    return (
        <button disabled={isPending} onClick={handleClick}>
            Delete selected posts
        </button>
    );
};

const BulkHardDeletePostsButton = () => {
    const { selectedIds } = useListContext();
    const [hardDeleteMany, { isPending, error }] = useHardDeleteMany();
    const handleClick = () => {
        hardDeleteMany({ ids: seletedIds });
    };
    if (error) {
        return <p>ERROR</p>;
    }
    return (
        <button disabled={isPending} onClick={handleClick}>
            Delete selected posts
        </button>
    );
};

TypeScript

The useHardDeleteMany hook accepts a generic parameter for the record type and another for the error type:

useHardDeleteMany<Product, Error>(undefined, undefined, {
    onError: (error) => {
        // TypeScript knows that error is of type Error
    },
    onSettled: (data, error) => {
        // TypeScript knows that data is of type Product['id'][]
        // TypeScript knows that error is of type Error
    },
});
useHardDeleteMany(undefined, undefined, {
    onError: (error) => {
        // TypeScript knows that error is of type Error
    },
    onSettled: (data, error) => {
        // TypeScript knows that data is of type Product['id'][]
        // TypeScript knows that error is of type Error
    },
});

useDeletedRecordsListController

useDeletedRecordsListController contains the headless logic of the <DeletedRecordsList> component. It's useful to create a custom deleted records list. It's also the base hook when building a custom view with another UI kit than Material UI.

useDeletedRecordsListController reads the deleted records list parameters from the URL, calls dataProvider.getListDeleted(), prepares callbacks for modifying the pagination, filters, sort and selection, and returns them together with the data. Its return value matches the ListContext shape.

Usage

useDeletedRecordsListController expects a parameters object defining the deleted records list sorting, pagination, and filters. It returns an object with the fetched data, and callbacks to modify the deleted records list parameters.

When using react-admin components, you can call useDeletedRecordsListController() without parameters, and to put the result in a ListContext to make it available to the rest of the component tree.

import { ListContextProvider } from 'react-admin';
import { useDeletedRecordsListController } from '@react-admin/ra-soft-delete';

const MyDeletedRecords = ({children}: { children: React.ReactNode }) => {
    const deletedRecordsListController = useDeletedRecordsListController();
    return (
        <ListContextProvider value={deletedRecordsListController}>
            {children}
        </ListContextProvider>
    );
};
import { ListContextProvider } from "react-admin";
import { useDeletedRecordsListController } from "@react-admin/ra-soft-delete";

const MyDeletedRecords = ({ children }) => {
    const deletedRecordsListController = useDeletedRecordsListController();
    return <ListContextProvider value={deletedRecordsListController}>{children}</ListContextProvider>;
};

Parameters

useDeletedRecordsListController expects an object as parameter. All keys are optional.

  • debounce: Debounce time in ms for the setFilters callbacks.
  • disableAuthentication: Set to true to allow anonymous access to the list
  • disableSyncWithLocation: Set to true to have more than one list per page
  • filter: Permanent filter, forced over the user filter
  • filterDefaultValues: Default values for the filter form
  • perPage: Number of results per page
  • queryOptions: React-query options for the useQuery call.
  • resource: The resource of deleted records to fetch and display (used as filter when calling getListDeleted)
  • sort: Current sort value, e.g. { field: 'deleted_at', order: 'ASC' }
  • storeKey: Key used to differentiate the list from another, in store managed states

Here are their default values:

import { ListContextProvider } from 'react-admin';
import { useDeletedRecordsListController } from '@react-admin/ra-soft-delete';

const CustomDeletedRecords = ({
    debounce = 500,
    disableAuthentication = false,
    disableSyncWithLocation = false,
    filter = undefined,
    filterDefaultValues = undefined,
    perPage = 10,
    queryOptions = undefined,
    sort = { field: 'deleted_at', order: 'DESC' },
    storeKey = undefined,
}) => {
    const deletedRecordsListController = useDeletedRecordsListController({
        debounce,
        disableAuthentication,
        disableSyncWithLocation,
        filter,
        filterDefaultValues,
        perPage,
        queryOptions,
        sort,
        storeKey,
    });
    return (
        <ListContextProvider value={deletedRecordsListController}>
            {children}
        </ListContextProvider>
    );
};
import { ListContextProvider } from "react-admin";
import { useDeletedRecordsListController } from "@react-admin/ra-soft-delete";

const CustomDeletedRecords = ({
    debounce = 500,
    disableAuthentication = false,
    disableSyncWithLocation = false,
    filter = undefined,
    filterDefaultValues = undefined,
    perPage = 10,
    queryOptions = undefined,
    sort = { field: "deleted_at", order: "DESC" },
    storeKey = undefined,
}) => {
    const deletedRecordsListController = useDeletedRecordsListController({
        debounce,
        disableAuthentication,
        disableSyncWithLocation,
        filter,
        filterDefaultValues,
        perPage,
        queryOptions,
        sort,
        storeKey,
    });
    return <ListContextProvider value={deletedRecordsListController}>{children}</ListContextProvider>;
};

storeKey

To display multiple deleted records lists and keep distinct store states for each of them (filters, sorting and pagination), specify unique keys with the storeKey property.

In case no storeKey is provided, the states will be stored with the following key: ra-soft-delete.listParams.

Note: Please note that selection state will remain linked to a constant key (ra-soft-delete.selectedIds) as described here.

If you want to disable the storage of list parameters altogether for a given list, you can use the disableSyncWithLocation prop.

In the example below, the controller states of NewestDeletedRecords and OldestDeletedRecords are stored separately (under the store keys 'newest' and 'oldest' respectively).

import { useDeletedRecordsListController } from '@react-admin/ra-soft-delete';

const OrderedDeletedRecords = ({
    storeKey,
    sort,
}) => {
    const params = useDeletedRecordsListController({
        sort,
        storeKey,
    });
    return (
        <ul>
            {!params.isPending &&
                params.data.map(deletedRecord => (
                    <li key={`deleted_record_${deletedRecord.id}`}>
                        [{deletedRecord.deleted_at}] Deleted by {deletedRecord.deleted_by}: <code>{JSON.stringify(deletedRecord.data)}</code>
                    </li>
                ))}
        </ul>
    );
};

const NewestDeletedRecords = (
    <OrderedDeletedRecords storeKey="newest" sort={{ field: 'deleted_at', order: 'DESC' }} />
);
const OldestDeletedRecords = (
    <OrderedDeletedRecords storeKey="oldest" sort={{ field: 'deleted_at', order: 'ASC' }} />
);
import { useDeletedRecordsListController } from "@react-admin/ra-soft-delete";

const OrderedDeletedRecords = ({ storeKey, sort }) => {
    const params = useDeletedRecordsListController({
        sort,
        storeKey,
    });
    return (
        <ul>
            {!params.isPending &&
                params.data.map((deletedRecord) => (
                    <li key={`deleted_record_${deletedRecord.id}`}>
                        [{deletedRecord.deleted_at}] Deleted by {deletedRecord.deleted_by}:{" "}
                        <code>{JSON.stringify(deletedRecord.data)}</code>
                    </li>
                ))}
        </ul>
    );
};

const NewestDeletedRecords = <OrderedDeletedRecords storeKey="newest" sort={{ field: "deleted_at", order: "DESC" }} />;
const OldestDeletedRecords = <OrderedDeletedRecords storeKey="oldest" sort={{ field: "deleted_at", order: "ASC" }} />;

You can disable this feature by setting the storeKey prop to false. When disabled, parameters will not be persisted in the store.

Return value

useDeletedRecordsListController returns an object with the following keys:

const {
    // Data
    data, // Array of the deleted records, e.g. [{ id: 123, resource: 'posts', deleted_at: '2025-03-25T12:32:22Z', deleted_by: 'test', data: { ... } }, { ... }, ...]
    total, // Total number of deleted records for the current filters, excluding pagination. Useful to build the pagination controls, e.g. 23      
    isPending, // Boolean, true until the data is available
    isFetching, // Boolean, true while the data is being fetched, false once the data is fetched
    isLoading, // Boolean, true until the data is fetched for the first time
    // Pagination
    page, // Current page. Starts at 1
    perPage, // Number of results per page. Defaults to 25
    setPage, // Callback to change the page, e.g. setPage(3)
    setPerPage, // Callback to change the number of results per page, e.g. setPerPage(25)
    hasPreviousPage, // Boolean, true if the current page is not the first one
    hasNextPage, // Boolean, true if the current page is not the last one
    // Sorting
    sort, // Sort object { field, order }, e.g. { field: 'deleted_at', order: 'DESC' }
    setSort, // Callback to change the sort, e.g. setSort({ field: 'id', order: 'ASC' })
    // Filtering
    filterValues, // Dictionary of filter values, e.g. { resource: 'posts', deleted_by: 'test' }
    setFilters, // Callback to update the filters, e.g. setFilters(filters)
    // Record selection
    selectedIds, // Array listing the ids of the selected deleted records, e.g. [123, 456]
    onSelect, // Callback to change the list of selected deleted records, e.g. onSelect([456, 789])
    onToggleItem, // Callback to toggle the deleted record selection for a given id, e.g. onToggleItem(456)
    onUnselectItems, // Callback to clear the deleted records selection, e.g. onUnselectItems();
    // Misc
    defaultTitle, // Translated title, e.g. 'Archives'
    refetch, // Callback for fetching the deleted records again
} = useDeletedRecordsListController();
const {
    // Data
    data, // Array of the deleted records, e.g. [{ id: 123, resource: 'posts', deleted_at: '2025-03-25T12:32:22Z', deleted_by: 'test', data: { ... } }, { ... }, ...]
    total, // Total number of deleted records for the current filters, excluding pagination. Useful to build the pagination controls, e.g. 23
    isPending, // Boolean, true until the data is available
    isFetching, // Boolean, true while the data is being fetched, false once the data is fetched
    isLoading, // Boolean, true until the data is fetched for the first time
    // Pagination
    page, // Current page. Starts at 1
    perPage, // Number of results per page. Defaults to 25
    setPage, // Callback to change the page, e.g. setPage(3)
    setPerPage, // Callback to change the number of results per page, e.g. setPerPage(25)
    hasPreviousPage, // Boolean, true if the current page is not the first one
    hasNextPage, // Boolean, true if the current page is not the last one
    // Sorting
    sort, // Sort object { field, order }, e.g. { field: 'deleted_at', order: 'DESC' }
    setSort, // Callback to change the sort, e.g. setSort({ field: 'id', order: 'ASC' })
    // Filtering
    filterValues, // Dictionary of filter values, e.g. { resource: 'posts', deleted_by: 'test' }
    setFilters, // Callback to update the filters, e.g. setFilters(filters)
    // Record selection
    selectedIds, // Array listing the ids of the selected deleted records, e.g. [123, 456]
    onSelect, // Callback to change the list of selected deleted records, e.g. onSelect([456, 789])
    onToggleItem, // Callback to toggle the deleted record selection for a given id, e.g. onToggleItem(456)
    onUnselectItems, // Callback to clear the deleted records selection, e.g. onUnselectItems();
    // Misc
    defaultTitle, // Translated title, e.g. 'Archives'
    refetch, // Callback for fetching the deleted records again
} = useDeletedRecordsListController();

Security

useDeletedRecordsListController requires authentication and will redirect anonymous users to the login page. If you want to allow anonymous access, use the disableAuthentication property.

If your authProvider implements Access Control, useDeletedRecordsListController will only render if the user has the deleted_records access on a virtual ra-soft-delete resource.

For instance, for the <CustomDeletedRecords> page below:

import { SimpleList } from 'react-admin';
import { useDeletedRecordsListController } from '@react-admin/ra-soft-delete';

const CustomDeletedRecords = () => {
    const { isPending, error, data, total } = useDeletedRecordsListController({ filter: { resource: 'posts' } })
    if (error) return <div>Error!</div>;
    return (
        <SimpleList
            data={data}
            total={total}
            isPending={isPending}
            primaryText="%{data.title}"
        />
    );
}
import { SimpleList } from "react-admin";
import { useDeletedRecordsListController } from "@react-admin/ra-soft-delete";

const CustomDeletedRecords = () => {
    const { isPending, error, data, total } = useDeletedRecordsListController({ filter: { resource: "posts" } });
    if (error) return <div>Error!</div>;
    return <SimpleList data={data} total={total} isPending={isPending} primaryText="%{data.title}" />;
};

useDeletedRecordsListController will call authProvider.canAccess() using the following parameters:

{ resource: 'ra-soft-delete', action: 'list_deleted_records' }
{
    resource: "ra-soft-delete", action;
    ("list_deleted_records");
}

Users without access will be redirected to the Access Denied page.

Note: Access control is disabled when you use the disableAuthentication property.

CHANGELOG

v1.0.0

2025-08-22

  • Initial release