Edit
The <Edit>
component is the main component for edition pages. It fetches a record based on the URL, prepares a form submit handler, and renders the page title and actions. It is not responsible for rendering the actual form - that’s the job of its child component (usually a form component, like <SimpleForm>
). This form component uses its children (<Input>
components) to render each form input.
The <Edit>
component calls dataProvider.getOne()
, using the id
from the URL. It creates a RecordContext
with the result. It also creates a SaveContext
containing a save
callback, which calls dataProvider.update()
when executed, and an EditContext
containing both the record and the callback.
Wrap the <Edit>
component around the form you want to create, then pass it as edit
prop of a given <Resource>
. <Edit>
requires no prop by default - it deduces the resource
and the id
from the current URL.
For instance, the following component will render an edition form for posts when users browse to /posts/edit/1234
:
// in src/customers.jsimport { Edit, SimpleForm, BooleanInput, TextInput } from "@/components/admin";import { required } from 'ra-core';
export const CustomerEdit = () => ( <Edit> <SimpleForm> <TextInput source="first_name" validate={required()} /> <TextInput source="last_name" validate={required()} /> <TextInput source="email" validate={required()} /> <BooleanInput source="has_ordered" /> <TextInput multiline source="notes" /> </SimpleForm> </Edit>);
// in src/App.jsimport { Admin } from '@/components/admin';import { Resource } from 'ra-core';
import { dataProvider } from './dataProvider';import { CustomerEdit } from './customers';
const App = () => ( <Admin dataProvider={dataProvider}> <Resource name="customers" edit={CustomerEdit} /> </Admin>);
export default App;
You can customize the <Edit>
component using the following props:
Prop | Required | Type | Default | Description |
---|---|---|---|---|
children | Optional * | ReactNode | - | The components that render the form |
render | Optional * | function | - | Function to render the form, receives the editContext as argument |
actions | Optional | ReactNode | - | Override the actions toolbar with a custom component |
className | Optional | string | - | Passed to the root component |
disableAuthentication | Optional | boolean | false | Disable the authentication check |
emptyWhileLoading | Optional | boolean | false | Set to true to return null while the edit is loading |
id | Optional | string /number | - | The id of the record to edit |
mutationMode | Optional | 'undoable' | 'optimistic' | 'pessimistic' | 'undoable' | Switch to optimistic or pessimistic mutations |
mutationOptions | Optional | object | - | Options for the dataProvider.update() call |
queryOptions | Optional | object | - | Options for the dataProvider.getOne() call |
redirect | Optional | 'list' | 'show' | false | function | 'list' | Change the redirect location after successful update |
resource | Optional | string | - | Override the name of the resource to edit |
title | Optional | string /ReactNode /false | - | Override the page title |
transform | Optional | function | - | Transform the form data before calling dataProvider.update() |
*
You must provide either children
or render
.
Main Content Area
Section titled “Main Content Area”The <Edit>
component will render its children inside a EditContext
provider, which the save
function. Children can be any React node, but are usually a form component like <SimpleForm>
, or the headless <Form>
component.
import { Edit, SimpleForm, TextInput, BooleanInput,} from "@/components/admin";import { required } from 'ra-core';
export const CustomerEdit = () => ( <Edit> <SimpleForm> <TextInput source="first_name" validate={required()} /> <TextInput source="last_name" validate={required()} /> <TextInput source="email" validate={required()} /> <BooleanInput source="has_ordered" /> <TextInput multiline source="notes" /> </SimpleForm> </Edit>);
Alternatively to children
, you can use a render
prop. It will receive the EditContext
as its argument, and should return a React node.
This allows to inline the render logic for the edition page.
export const PostEdit = () => ( <Edit render={({ isPending, record, save, saving }) => ( <div> <h1>Edit Post</h1> {!isPending && ( <form onSubmit={save}> <input type="text" name="title" defaultValue={record.title} required /> <textarea name="teaser" defaultValue={record.teaser} rows={3} /> <textarea name="body" defaultValue={record.body} rows={5} /> <input type="date" name="published_at" defaultValue={redord.published_at} /> <button type="submit" disabled={saving}> {saving ? 'Saving...' : 'Save'} </button> </form> )} </div> )} />);
Tip: When receiving a render
prop, the <Edit>
component will ignore the children
prop.
Actions Toolbar
Section titled “Actions Toolbar”By default, the Edit component includes a toolbar with a button to navigate to the show view (if present) and a button to delete the current record.
You can replace these actions by your own elements using the actions
prop:
import { Edit, ShowButton, CreateButton, DeleteButton } from '@/components/admin';
const PostEditActions = () => ( <div className="flex items-center gap-2"> <ShowButton /> <CreateButton /> <DeleteButton /> </div>);
export const PostEdit = () => ( <Edit actions={<PostEditActions />}> ... </Edit>);
Common buttons used as Edit actions are:
<CreateButton>
to create a new record<ListButton>
to go back to the list<ShowButton>
to go to the show page<DeleteButton>
to delete the current record
And you can add your own button, leveraging the useRecordContext()
hook:
import { useRecordContext, useUpdate, useNotify } from 'ra-core';import { Button } from '@a/components/ui';
const ResetViewsButton = () => { const record = useRecordContext(); const [update, { isPending }] = useUpdate(); const notify = useNotify(); const handleClick = () => { update( 'posts', { id: record.id, data: { views: 0 }, previousData: record }, { onSuccess: () => { notify('Views reset'); }, onFailure: error => notify(`Error: ${error.message}`, 'warning'), } ); }; return ( <Button onClick={handleClick} disabled={isPending}> Reset views </Button> );};
Page Title
Section titled “Page Title”By default, the title for the Edit view is the translation key ra.page.edit
that translates to “Edit [resource_name] [record representation]”. Check the <Resource recordRepresentation>
prop for more details.
You can customize this title by providing a resource specific translation with the key resources.RESOURCE.page.edit
(e.g. resources.posts.page.edit
):
// in src/i18n/en.jsimport englishMessages from 'ra-language-english';
export const en = { ...englishMessages, resources: { posts: { name: 'Post |||| Posts', page: { edit: 'Update post "%{recordRepresentation}"' } }, }, ...};
You can also customize this title by specifying a custom title
string:
export const PostEdit = () => ( <Edit title="Edit post"> ... </Edit>);
More interestingly, you can pass an element as title
. This element can access the current record via useRecordContext
. This allows to customize the title according to the current record:
const PostTitle = () => { const record = useRecordContext(); return <span>Post {record ? `"${record.title}"` : ''}</span>;};
export const PostEdit = () => ( <Edit title={<PostTitle />}> ... </Edit>);
Finally, you can also pass false
to disable the title:
export const PostEdit = () => ( <Edit title={false}> ... </Edit>);
Data Fetching Options
Section titled “Data Fetching Options”<Edit>
calls dataProvider.getOne()
on mount via react-query’s useQuery
hook. You can customize the options you pass to this hook by setting the queryOptions
prop.
This can be useful e.g. to pass a custom meta
to the dataProvider.getOne()
call.
import { Edit, SimpleForm } from '@/components/admin';
export const PostEdit = () => ( <Edit queryOptions={{ meta: { foo: 'bar' } }}> <SimpleForm> ... </SimpleForm> </Edit>);
You can also use queryOptions
to force a refetch on reconnect:
const PostEdit = () => ( <Edit queryOptions={{ refetchOnReconnect: true }}> ... </Edit>);
Refer to the useQuery documentation in the react-query website for a list of the possible options.
Mutation Options
Section titled “Mutation Options”<Edit>
calls dataProvider.update()
via react-query’s useMutation
hook. You can customize the options you pass to this hook, e.g. to pass a custom meta
to the dataProvider.update()
call.
import { Edit, SimpleForm } from '@/components/admin';
const PostEdit = () => ( <Edit mutationOptions={{ meta: { foo: 'bar' } }}> <SimpleForm> ... </SimpleForm> </Edit>);
Refer to the useMutation documentation in the react-query website for a list of the possible options.
You can also use mutationOptions
to override success or error side effects.
Success and Error Side Effects
Section titled “Success and Error Side Effects”By default, when the save action succeeds, Shadcn Admin Kit shows a notification, and redirects to the list page.
You can override this behavior and pass custom success side effects by providing a mutationOptions
prop with an onSuccess
key:
import { Edit, SimpleForm } from '@/components/admin';import { useNotify, useRefresh, useRedirect } from 'ra-core';
const PostEdit = () => { const notify = useNotify(); const refresh = useRefresh(); const redirect = useRedirect();
const onSuccess = () => { notify(`Changes saved`); redirect('/posts'); refresh(); };
return ( <Edit mutationOptions={{ onSuccess }}> <SimpleForm> ... </SimpleForm> </Edit> );}
The default onSuccess
function is:
() => { notify('ra.notification.updated', { messageArgs: { smart_count: 1 }, undoable: mutationMode === 'undoable' }); redirect('list', resource, data.id, data);}
Similarly, you can override the failure side effects with an onError
option. By default, when the save action fails at the dataProvider level, Shadcn Admin Kit shows a notification error.
import { Edit, SimpleForm } from '@/components/admin';import { useNotify, useRefresh, useRedirect } from 'ra-core';
const PostEdit = () => { const notify = useNotify(); const refresh = useRefresh(); const redirect = useRedirect();
const onError = (error) => { notify(`Could not edit post: ${error.message}`); redirect('/posts'); refresh(); };
return ( <Edit mutationOptions={{ onError }}> <SimpleForm> ... </SimpleForm> </Edit> );}
The onError
function receives the error from the dataProvider.update()
call. It is a JavaScript Error object (see the dataProvider documentation for details).
The default onError
function is:
(error) => { notify(typeof error === 'string' ? error : error.message || 'ra.notification.http_error', { type: 'error' }); if (mutationMode === 'undoable' || mutationMode === 'pessimistic') { refresh(); }}
Changing The Notification Message
Section titled “Changing The Notification Message”Once the dataProvider.update()
request returns successfully, users see a success notification.
<Edit>
uses two successive translation keys to build the success message:
resources.{resource}.notifications.updated
as a first choicera.notification.updated
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 Shadcn Admin Kit uses the same translation message for the <BulkUpdateButton>
, so the message must support pluralization:
const englishMessages = { resources: { orders: { notifications: { updated: 'Order updated |||| %{smart_count} orders updated', // ... }, }, },};
Alternately, you can customize this message by passing a custom success side effect function in the mutationOptions
prop:
import { Edit, SimpleForm } from '@/components/admin';import { useNotify, useRedirect } from 'ra-core';
const OrderEdit = () => { const notify = useNotify(); const redirect = useRedirect();
const onSuccess = () => { notify(`Order updated successfully`); redirect('list', 'orders'); };
return ( <Edit mutationOptions={{ onSuccess }}> <SimpleForm> ... </SimpleForm> </Edit> );}
You can do the same for error notifications, by passing a custom onError
callback.
Redirection After Submission
Section titled “Redirection After Submission”By default, submitting the form in the <Edit>
view redirects to the <List>
view.
You can customize the redirection by setting the redirect
prop to one of the following values:
'list'
: redirect to the List view (the default)'show'
: redirect to the Show viewfalse
: do not redirect- A function
(resource, id, data) => string
to redirect to different targets depending on the record
const PostEdit = () => ( <Edit redirect="show"> ... </Edit>);
Mutation Mode
Section titled “Mutation Mode”The <Edit>
view exposes two buttons, Save and Delete, which perform “mutations” (i.e. they alter the data). Shadcn Admin Kit 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, pages using <Edit>
use the undoable
mutation mode. This is part of the “optimistic rendering” strategy of Shadcn Admin Kit; it makes user interactions more reactive.
You can change this default by setting the mutationMode
prop - and this affects both the Save and Delete buttons. For instance, to remove the ability to undo the changes, use the optimistic
mode:
const PostEdit = () => ( <Edit mutationMode="optimistic"> // ... </Edit>);
And to make both the Save and Delete actions blocking, and wait for the dataProvider response to continue, use the pessimistic
mode:
const PostEdit = () => ( <Edit mutationMode="pessimistic"> // ... </Edit>);
Transforming Data
Section titled “Transforming Data”To transform a record after the user has submitted the form but before the record is passed to dataProvider.update()
, use the transform
prop. It expects a function taking a record as argument, and returning a modified record. For instance, to add a computed field upon edition:
export const UserEdit = () => { const transform = data => ({ ...data, fullName: `${data.firstName} ${data.lastName}` }); return ( <Edit transform={transform}> ... </Edit> );}
The transform
function can also return a Promise
, which allows you to do all sorts of asynchronous calls (e.g. to the dataProvider
) during the transformation.
Scaffolding An Edit Page
Section titled “Scaffolding An Edit Page”You can use <EditGuesser>
to quickly bootstrap an Edit view on top of an existing API, without adding the inputs one by one.
// in src/App.jsimport { Admin, EditGuesser } from '@/components/admin';import { Resource } from 'ra-core';import dataProvider from './dataProvider';
const App = () => ( <Admin dataProvider={dataProvider}> {/* ... */} <Resource name="customers" edit={EditGuesser} /> </Admin>);
Just like <Edit>
, <EditGuesser>
fetches the data. It then analyzes the response, and guesses the inputs it should use to display a basic <SimpleForm>
with the data. It also dumps the components it has guessed in the console, so you can copy it into your own code.
Cleaning Up Empty Strings
Section titled “Cleaning Up Empty Strings”As a reminder, HTML form inputs always return strings, even for numbers and booleans. So the empty value for a text input is the empty string, not null
or undefined
. This means that the data sent to dataProvider.update()
will contain empty strings:
{ title: '', average_note: '', body: '', // etc.}
If you prefer to have null
values, or to omit the key for empty values, use the transform
prop to sanitize the form data before submission:
export const UserEdit = () => { const transform = (data) => { const sanitizedData = {}; for (const key in data) { if (typeof data[key] === "string" && data[key].trim().length === 0) continue; sanitizedData[key] = data[key]; } return sanitizedData; }; return ( <Edit transform={transform}> ... </Edit> );}
As an alternative, you can clean up empty values at the input level, using the parse
prop.
Prefilling the Form
Section titled “Prefilling the Form”You sometimes need to pre-populate the form changes to a record. For instance, to revert a record to a previous version, or to make some changes while letting users modify others fields as well.
By default, the <Edit>
view starts with the current record
. However, if the location
object (injected by react-router-dom) contains a record
in its state
, the <Edit>
view uses that record
to prefill the form.
That means that if you want to create a link to an edition view, modifying immediately some values, all you have to do is to set the state
prop of the <EditButton>
:
import * as React from 'react';import { EditButton, DataTable, List } from '@/components/admin';
const ApproveButton = () => ( <EditButton state={{ record: { status: 'approved' } }} />);
export default PostList = () => ( <List> <DataTable> ... <DataTable.Col> <ApproveButton /> </DataTable.Col> </DataTable> </List>)
Should you use the location state
or the location search
? The latter modifies the URL, so it’s only necessary if you want to build cross-application links (e.g. from one admin to the other). In general, using the location state
is a safe bet.
Linking Two Inputs
Section titled “Linking Two Inputs”Edition forms often contain linked inputs, e.g. country and city (the choices of the latter depending on the value of the former).
Shadcn Admin Kit relies on react-hook-form for form handling. You can grab the current form values using react-hook-form’s useWatch hook.
import { Edit, SimpleForm, SelectInput } from '@/components/admin';import { useWatch } from 'react-hook-form';
const countries = ['USA', 'UK', 'France'];const cities = { USA: ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix'], UK: ['London', 'Birmingham', 'Glasgow', 'Liverpool', 'Bristol'], France: ['Paris', 'Marseille', 'Lyon', 'Toulouse', 'Nice'],};const toChoices = items => items.map(item => ({ id: item, name: item }));
const CityInput = () => { const country = useWatch({ name: 'country' }); return ( <SelectInput choices={country ? toChoices(cities[country]) : []} source="cities" /> );};
const OrderEdit = () => ( <Edit> <SimpleForm> <SelectInput source="country" choices={toChoices(countries)} /> <CityInput /> </SimpleForm> </Edit>);
export default OrderEdit;
Controlled Mode
Section titled “Controlled Mode”<Edit>
deduces the resource and the record id from the URL. This is fine for an edition page, but if you need to let users edit records from another page, you probably want to define the edit parameters yourself.
In that case, use the resource
and id
props to set the edit parameters regardless of the URL.
import { Edit, SimpleForm, TextInput, SelectInput } from "@/components/admin";
export const BookEdit = ({ id }) => ( <Edit resource="books" id={id} redirect={false}> <SimpleForm> <TextInput source="title" /> <TextInput source="author" /> <SelectInput source="availability" choices={[ { id: "in_stock", name: "In stock" }, { id: "out_of_stock", name: "Out of stock" }, { id: "out_of_print", name: "Out of print" }, ]} /> </SimpleForm> </Edit>);
Headless Version
Section titled “Headless Version”Besides fetching a record and preparing a save handler, <Edit>
renders the default edition page layout (title, actions, a Material UI <Card>
) and its children. If you need a custom edition layout, you may prefer the <EditBase>
component, which only renders its children in an EditContext
.
import { EditBase } from "ra-core";import { SelectInput, SimpleForm, TextInput} from "@/components/admin";import { Card, CardContent } from "@/commponents/ui";
export const BookEdit = () => ( <EditBase> <h1>Book Edition</h1> <Card> <CardContent> <SimpleForm> <TextInput source="title" /> <TextInput source="author" /> <SelectInput source="availability" choices={[ { id: "in_stock", name: "In stock" }, { id: "out_of_stock", name: "Out of stock" }, { id: "out_of_print", name: "Out of print" }, ]} /> </SimpleForm> </CardContent> </Card> </EditBase>);
In the previous example, <SimpleForm>
grabs the record and the save handler from the EditContext
.
If you don’t need the EditContext
, you can use the useEditController
hook, which does the same data fetching as <EditBase>
but lets you render the content.
import { useEditController } from "ra-core";import { SelectInput, SimpleForm, TextInput } from "@/components/admin";import { Card, CardContent } from "@/components/ui";
export const BookEdit = () => { const { record, save } = useEditController(); return ( <> <h1> Edit book {record?.title} </h1> <Card> <CardContent> <SimpleForm record={record} onSubmit={values => save(values)}> <TextInput source="title" /> <TextInput source="author" /> <SelectInput source="availability" choices={[ { id: "in_stock", name: "In stock" }, { id: "out_of_stock", name: "Out of stock" }, { id: "out_of_print", name: "Out of print" }, ]} /> </SimpleForm> </CardContent> </Card> </> );};
Access Control
Section titled “Access Control”If your authProvider
implements Access Control, <Edit>
will only render if the user has the “edit” access to the related resource.
For instance, for the <PostEdit>
page below:
import { Edit, SimpleForm, TextInput } from '@/components/admin';
// Resource name is "posts"const PostEdit = () => ( <Edit> <SimpleForm> <TextInput source="title" /> <TextInput source="author" /> <TextInput source="published_at" /> </SimpleForm> </Edit>);
<Edit>
will call authProvider.canAccess()
using the following parameters:
{ action: "edit", resource: "posts" }
Users without access will be redirected to the Access Denied page.
:::note*
Access control is disabled when you use the disableAuthentication
prop.
:::