ra-soft-delete
Keep deleted records to recover them later on. List all deleted records, inspect them, and restore or delete items permanently.
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 callscreate
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 singledeleted_records
(configurable) resource.addSoftDeleteInPlace
keeps the deleted records in the same resource, but fillsdeleted_at
(configurable) anddeleted_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:
softDelete
:useSoftDelete
softDeleteMany
:useSoftDeleteMany
getListDeleted
:useGetListDeleted
getOneDeleted
:useGetOneDeleted
restoreOne
:useRestoreOne
restoreMany
:useRestoreMany
hardDelete
:useHardDelete
hardDeleteMany
:useHardDeleteMany
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 aresource
filter, as it uses a naive implementation combining multiplegetList
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>
);
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.
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 choicera-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.
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 choicera-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.
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>
);
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>
.
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 thesetFilters
callbacks.disableAuthentication
: Set to true to allow anonymous access to the listdisableSyncWithLocation
: Set to true to have more than one list per pagefilter
: Permanent filter, forced over the user filterfilterDefaultValues
: Default values for the filter formperPage
: Number of results per pagequeryOptions
: React-query options for theuseQuery
call.resource
: The resource of deleted records to fetch and display (used as filter when callinggetListDeleted
)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