Skip to content

<EditBase>

<EditBase> is a headless coFor instance, to display several fields in a single line, you can use native HTML layout:ponent that fetches a record based on the URL, prepares a form submit handler, and renders its children inside an EditContext. Use it to build a custom edition page layout.

<EditBase> relies on the useEditController hook.

Use <EditBase> to create a custom Edition view, with exactly the content you add as child and nothing else (no title, card, or list of actions as in the Edit component).

import * as React from "react";
import { EditBase, Form } from "ra-core";
import { TextInput } from './TextInput';
import { SelectInput } from './SelectInput';
export const BookEdit = () => (
<EditBase>
<div>
<h1>Book Edition</h1>
<div>
<Form>
<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" },
]} />
</Form>
</div>
</div>
</EditBase>
);
PropRequiredTypeDefaultDescription
childrenOptionalReactNodeThe components rendering the record fields
renderOptional(props: EditControllerResult<RecordType>) => ReactNodeAlternative to children, a function that takes the EditController context and renders the form
disable AuthenticationOptionalbooleanSet to true to disable the authentication check
idOptionalstringThe record identifier. If not provided, it will be deduced from the URL
loadingOptionalReactNodeThe component to render while checking for authentication and permissions
mutationModeOptionalundoableThe mutation mode
mutationOptionsOptionalReactNodeThe options to pass to the useUpdate hook
offlineOptionalReactNodeThe component to render when there is no connectivity and the record isn’t in the cache
queryOptionsOptionalobjectThe options to pass to the useGetOne hook
transformOptionalstringTransform the form data before calling dataProvider.update()

<EditBase> renders its children wrapped by a RecordContext, so you can use any component that depends on such a context to be defined, for example inputs leveraging the useInput hook.

import { EditBase, Form } from 'ra-core';
import { TextInput } from './TextInput';
import { DateInput } from './DateInput';
const BookEdit = () => (
<EditBase>
<Form>
<div style={{ display: 'flex', gap: '1rem', margin: '1rem' }}>
<div style={{ flex: 1 }}>
<TextInput label="Title" source="title" />
</div>
<div style={{ flex: 1 }}>
<TextInput label="Author" source="author" />
</div>
<div style={{ flex: 1 }}>
<DateInput label="Publication Date" source="published_at" />
</div>
</div>
<div style={{ margin: '1rem' }}>
<button type="submit">Save</button>
</div>
</Form>
</EditBase>
);

By default, the <EditBase> component will automatically redirect the user to the login page if the user is not authenticated. If you want to disable this behavior and allow anonymous access to a show page, set the disableAuthentication prop to true.

import { EditBase, Form } from 'ra-core';
const PostEdit = () => (
<EditBase disableAuthentication>
<Form>
{/* form content */}
</Form>
</EditBase>
);

By default, <EditBase> deduces the identifier of the record to show from the URL path. So under the /posts/123/show path, the id prop will be 123. You may want to force a different identifier. In this case, pass a custom id prop.

import { EditBase, Form } from 'ra-core';
export const PostEdit = () => (
<EditBase id="123">
<Form>
{/* form content */}
</Form>
</EditBase>
);

Tip: Pass both a custom id and a custom resource prop to use <EditBase> independently of the current URL. This even allows you to use more than one <EditBase> component in the same page.

By default, <EditBase> renders nothing while checking for authentication and permissions. You can provide your own component via the loading prop:

import { EditBase, Form } from 'ra-core';
export const PostEdit = () => (
<EditBase loading={<p>Checking for permissions...</p>}>
<Form>
{/* form content */}
</Form>
</EditBase>
);

The <EditBase> component exposes a save method, which perform a “mutation” (i.e. they alter the data). Ra-core 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 <EditBase> use the undoable mutation mode. This is part of the “optimistic rendering” strategy of ra-core ; 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:

import { EditBase, Form } from 'ra-core';
const PostEdit = () => (
<EditBase mutationMode="optimistic">
<Form>
{/* form content */}
</Form>
</EditBase>
);

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

import { EditBase, Form } from 'ra-core';
const PostEdit = () => (
<EditBase mutationMode="pessimistic">
<Form>
{/* form content */}
</Form>
</EditBase>
);

<EditBase> 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 { EditBase, Form } from 'ra-core';
const PostEdit = () => (
<EditBase mutationOptions={{ meta: { foo: 'bar' } }}>
<Form>
{/* form content */}
</Form>
</EditBase>
);

You can also use mutationOptions to override success or error side effects, by setting the mutationOptions prop. Refer to the useMutation documentation in the react-query website for a list of the possible options.

Let’s see an example with the success side effect. By default, when the save action succeeds, ra-core 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 * as React from 'react';
import { useNotify, useRefresh, useRedirect, EditBase, Form } from 'ra-core';
const PostEdit = () => {
const notify = useNotify();
const refresh = useRefresh();
const redirect = useRedirect();
const onSuccess = () => {
notify(`Changes saved`);
redirect('/posts');
refresh();
};
return (
<EditBase mutationOptions={{ onSuccess }}>
<Form>
{/* form content */}
</Form>
</EditBase>
);
}

The default onSuccess function is:

() => {
notify('ra.notification.updated', {
messageArgs: { smart_count: 1 },
undoable: mutationMode === 'undoable'
});
redirect('list', resource, data.id, data);
}

Tip: If you just want to customize the redirect behavior, you can use the redirect prop instead.

Tip: When you use mutationMode="pessimistic", the onSuccess function receives the response from the dataProvider.update() call, which is the created/edited record (see the dataProvider documentation for details). You can use that response in the success side effects:

import * as React from 'react';
import { useNotify, useRefresh, useRedirect, EditBase, Form } from 'ra-core';
const PostEdit = () => {
const notify = useNotify();
const refresh = useRefresh();
const redirect = useRedirect();
const onSuccess = (data) => {
notify(`Changes to post "${data.title}" saved`);
redirect('/posts');
refresh();
};
return (
<EditBase mutationOptions={{ onSuccess }} mutationMode="pessimistic">
<Form>
{/* form content */}
</Form>
</EditBase>
);
}

Similarly, you can override the failure side effects with an onError option. By default, when the save action fails at the dataProvider level, ra-core shows a notification error.

import * as React from 'react';
import { useNotify, useRefresh, useRedirect, EditBase, Form } 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 (
<EditBase mutationOptions={{ onError }}>
<Form>
{/* form content */}
</Form>
</EditBase>
);
}

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();
}
}

By default, <EditBase> renders nothing when there is no connectivity and the record hasn’t been cached yet. You can provide your own component via the offline prop:

import { EditBase } from 'ra-core';
export const PostEdit = () => (
<EditBase offline={<p>No network. Could not load the post.</p>}>
...
</EditBase>
);

Tip: If the record is in the Tanstack Query cache but you want to warn the user that they may see an outdated version, you can use the <IsOffline> component:

import { EditBase, IsOffline } from 'ra-core';
export const PostEdit = () => (
<EditBase offline={<p>No network. Could not load the post.</p>}>
<IsOffline>
No network. The post data may be outdated.
</IsOffline>
...
</EditBase>
);

<EditBase> accepts a queryOptions prop to pass options to the react-query client.

This can be useful e.g. to override the default error side effect. By default, when the dataProvider.getOne() call fails at the dataProvider level, ra-core shows an error notification and refreshes the page.

You can override this behavior and pass custom side effects by providing a custom queryOptions prop:

import * as React from 'react';
import { useNotify, useRefresh, useRedirect, EditBase, Form } from 'ra-core';
const PostEdit = () => {
const notify = useNotify();
const refresh = useRefresh();
const redirect = useRedirect();
const onError = (error) => {
notify(`Could not load post: ${error.message}`, { type: 'error' });
redirect('/posts');
refresh();
};
return (
<EditBase queryOptions={{ onError }}>
<Form>
{/* form content */}
</Form>
</EditBase>
);
}

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

The default onError function is:

(error) => {
notify('ra.notification.item_doesnt_exist', { type: 'error' });
redirect('list', resource);
refresh();
}

Alternatively, you can pass a render function prop instead of children. This function will receive the EditContext as argument.

import { EditBase, Form } from 'ra-core';
import { TextInput } from './TextInput';
import { DateInput } from './DateInput';
const BookEdit = () => (
<EditBase render={({ isPending, error }) => {
if (isPending) {
return <p>Loading...</p>;
}
if (error) {
return (
<p className="error">
{error.message}
</p>
);
}
return (
<Form>
<div style={{ display: 'flex', gap: '1rem', margin: '1rem' }}>
<div style={{ flex: 1 }}>
<TextInput label="Title" source="title" />
</div>
<div style={{ flex: 1 }}>
<TextInput label="Author" source="author" />
</div>
<div style={{ flex: 1 }}>
<DateInput label="Publication Date" source="published_at" />
</div>
</div>
<div style={{ margin: '1rem' }}>
<button type="submit">Save</button>
</div>
</Form>
);
}}/>
);

By default, <EditBase> operates on the current ResourceContext (defined at the routing level), so under the /posts/1/show path, the resource prop will be posts. You may want to force a different resource. In this case, pass a custom resource prop, and it will override the ResourceContext value.

import { EditBase, Form } from 'ra-core';
export const UsersEdit = () => (
<EditBase resource="users">
<Form>
{/* form content */}
</Form>
</EditBase>
);

Tip: Pass both a custom id and a custom resource prop to use <EditBase> independently of the current URL. This even allows you to use more than one <EditBase> component in the same page.

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 update:

export const UserEdit = () => {
const transform = data => ({
...data,
fullName: `${data.firstName} ${data.lastName}`
});
return (
<EditBase transform={transform}>
<Form>
{/* form content */}
</Form>
</EditBase>
);
}

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.

The <EditBase> component requires authentication and will redirect anonymous users to the login page. If you want to allow anonymous access, use the disableAuthentication prop.

If your authProvider implements Access Control, <EditBase> will only render if the user has the “edit” access to the related resource.

For instance, for the <PostEdit>page below:

import { EditBase, Form } from 'ra-core';
import { TextInput } from './TextInput';
// Resource name is "posts"
const PostEdit = () => (
<EditBase>
<Form>
<TextInput source="title" />
<TextInput source="author" />
<TextInput source="published_at" />
</Form>
</EditBase>
);

<EditBase> 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.

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 other fields as well.

By default, the <EditBase> view starts with the current record. However, if the location object (injected by react-router-dom) contains a record in its state, the <EditBase> 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 when navigating to the edit route:

import * as React from 'react';
import { useNavigate } from 'react-router-dom';
import { useRecordContext } from 'ra-core';
const ApproveButton = () => {
const record = useRecordContext();
const navigate = useNavigate();
const handleClick = () => {
navigate(`/posts/${record.id}`, {
state: { record: { status: 'approved' } }
});
};
return (
<button onClick={handleClick}>
Approve
</button>
);
};

Tip: The <EditBase> component also watches the “source” parameter of location.search (the query string in the URL) in addition to location.state (a cross-page message hidden in the router memory). So the ApproveButton could also be written as:

import * as React from 'react';
import { useNavigate } from 'react-router-dom';
import { useRecordContext } from 'ra-core';
const ApproveButton = () => {
const record = useRecordContext();
const navigate = useNavigate();
const handleClick = () => {
navigate(`/posts/${record.id}?source=${JSON.stringify({ status: 'approved' })}`);
};
return (
<button onClick={handleClick}>
Approve
</button>
);
};

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.

You can detect prefilled values by leveraging the useRecordFromLocation hook:

import { EditBase, Form, useRecordFromLocation } from 'ra-core';
import { TextInput } from './TextInput';
const PostEdit = () => {
const recordFromLocation = useRecordFromLocation();
return (
<EditBase>
{recordFromLocation && (
<div
style={{
padding: '12px 16px',
backgroundColor: '#fff3cd',
border: '1px solid #ffeaa7',
borderRadius: '4px',
marginBottom: '16px',
color: '#856404'
}}
>
Some fields have been pre-filled. You can modify them before saving.
</div>
)}
<Form>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<TextInput source="title" />
<TextInput source="author" />
<TextInput source="status" />
</div>
</Form>
</EditBase>
);
};