The Creation and Edition Pages

React-admin provides many hooks and components to let you build custom user experiences for editing and creating records, leveraging Material UI and react-hook-form.

Edit view example

From Pure React To React-Admin

Edition views are very common in single-page apps. The most usual way to allow a user to update a record is to fetch the record from an API based on the URL parameters, initialize a form with the record, update the inputs as the user changes the values, and call the API to update the record with the new values upon submission.

From Pure React To React-Admin

To better understand how to use the various react-admin hooks and components dedicated to editing and creating, let’s start by building such an edition view by hand.

An Edition View Built By Hand

Here is how you could write a book edition view in pure React, leveraging react-admin’s data fetching hooks, and react-hook-form to bind form inputs with a record object:

import * as React from "react";
import { useParams, useNavigate } from "react-router-dom";
import { useForm, Controller } from "react-hook-form";
import { useGetOne, useUpdate, Title } from "react-admin";
import { Card, TextField, Button, Stack, MenuItem } from "@mui/material";

export const BookEdit = () => {
  const { id } = useParams();
  const { handleSubmit, reset, control } = useForm();
  const { isLoading } = useGetOne(
    "books",
    { id },
    { onSuccess: (data) => reset(data) }
  );
  const [update, { isLoading: isSubmitting }] = useUpdate();
  const navigate = useNavigate();
  const onSubmit = (data) => {
    update(
        "books",
        { id, data },
        { onSuccess: () => { navigate('/books'); } }
    );
  };

  if (isLoading) return null;
  return (
    <div>
      <Title title="Book Edition" />
      <Card>
        <form onSubmit={handleSubmit(onSubmit)}>
          <Stack spacing={2}>
            <Controller
              name="title"
              render={({ field }) => <TextField label="Title" {...field} />}
              control={control}
            />
            <Controller
              name="author"
              render={({ field }) => <TextField label="Author" {...field} />}
              control={control}
            />
            <Controller
              name="availability"
              render={({ field }) => (
                <TextField select label="Availability" {...field}>
                  <MenuItem value="in_stock">In stock</MenuItem>
                  <MenuItem value="out_of_stock">Out of stock</MenuItem>
                  <MenuItem value="out_of_print">Out of print</MenuItem>
                </TextField>
              )}
              control={control}
            />
            <Button type="submit" disabled={isSubmitting}>
              Save
            </Button>
          </Stack>
        </form>
      </Card>
    </div>
  );
};

This form displays 3 inputs (two text inputs and one select input), and redirects to the book list view upon successful submission. It doesn’t even contain default values, validation, or dependent inputs. Yet, it’s already quite verbose.

It’s a super common component. In fact, many of its features could be extracted for reuse in other pages. Let’s see how to improve the code and the developer experience in the same process.

<Form>: Form Logic

To use react-hook-form with Material UI inputs, the previous example leverages the <Controller> tag, which expects a control object generated by the useForm hook (see the related react-hook-form doc).

We can avoid the call to useForm by putting its logic inside a custom component. That’s exaclty what react-admin’s <Form> component does. <Form> also creates a react-hook-form <FormProvider>, so we no longer need to pass the control prop to each <Controller> element.

import * as React from "react";
import { useParams, useNavigate } from "react-router-dom";
-import { useForm, Controller } from "react-hook-form";
+import { Controller } from "react-hook-form";
-import { useGetOne, useUpdate, Title } from "react-admin";
+import { useGetOne, useUpdate, Title, Form } from "react-admin";
import { Card, TextField, Stack, MenuItem } from "@mui/material";

export const BookEdit = () => {
  const { id } = useParams();
- const { handleSubmit, reset, control } = useForm();
- const { isLoading } = useGetOne(
+ const { isLoading, data } = useGetOne(
    "books",
    { id },
-   { onSuccess: (data) => reset(data) }
  );
  const [update, { isLoading: isSubmitting }] = useUpdate();
  const navigate = useNavigate();
  const onSubmit = (data) => {
    update(
        "books",
        { id, data },
        { onSuccess: () => { navigate('/books'); } }
    );
  };
  if (isLoading) return null;
  return (
    <div>
      <Title title="Book Edition" />
      <Card>
-       <form onSubmit={handleSubmit(onSubmit)}>
+       <Form record={data} onSubmit={onSubmit}>
          <Stack spacing={2}>
            <Controller
              name="title"
              render={({ field }) => <TextField label="Title" {...field} />}
-             control={control}
            />
            <Controller
              name="author"
              render={({ field }) => <TextField label="Author" {...field} />}
-             control={control}
            />
            <Controller
              name="availability"
              render={({ field }) => (
                <TextField select label="Availability" {...field}>
                  <MenuItem value="in_stock">In stock</MenuItem>
                  <MenuItem value="out_of_stock">Out of stock</MenuItem>
                  <MenuItem value="out_of_print">Out of print</MenuItem>
                </TextField>
              )}
-             control={control}
            />
            <Button type="submit" disabled={isSubmitting}>
              Save
            </Button>
          </Stack>
-       </form>
+       </Form>
      </Card>
    </div>
  );
};

<SimpleForm>: Stacked Layout

Displaying inputs in a Stack is a common UI pattern. The <SimpleForm> component is a convenience wrapper around <Form> that provides this stacked layout. It also includes a submit button, so the BookEdit component code is now more focused on business logic.

import * as React from "react";
import { useParams, useNavigate } from "react-router-dom";
import { Controller } from "react-hook-form";
-import { useGetOne, useUpdate, Title, Form } from "react-admin";
+import { useGetOne, useUpdate, Title, SimpleForm } from "react-admin";
-import { Card, TextField, Stack, MenuItem } from "@mui/material";
+import { Card, TextField, MenuItem } from "@mui/material";

export const BookEdit = () => {
  const { id } = useParams();
  const { isLoading, data } = useGetOne("books", { id });
  const [update, { isLoading: isSubmitting }] = useUpdate();
  const navigate = useNavigate();
  const onSubmit = (data) => {
    update(
        "books",
        { id, data },
        { onSuccess: () => { navigate('/books'); } }
    );
  };
  if (isLoading) return null;
  return (
    <div>
      <Title title="Book Edition" />
      <Card>
-       <Form record={data} onSubmit={onSubmit}>
+       <SimpleForm record={data} onSubmit={onSubmit}>
-         <Stack spacing={2}>
          <Controller
            name="title"
            render={({ field }) => <TextField label="Title" {...field} />}
          />
          <Controller
            name="author"
            render={({ field }) => <TextField label="Author" {...field} />}
          />
          <Controller
            name="availability"
            render={({ field }) => (
              <TextField select label="Availability" {...field}>
                <MenuItem value="in_stock">In stock</MenuItem>
                <MenuItem value="out_of_stock">Out of stock</MenuItem>
                <MenuItem value="out_of_print">Out of print</MenuItem>
              </TextField>
            )}
          />
-         <Button type="submit" disabled={isSubmitting}>
-           Save
-         </Button>
-       </Stack>
-       </Form>
+       </SimpleForm>
      </Card>
    </div>
  );
};

React-admin proposes alternative form layouts (<TabbedForm>, <AccordionForm>, <WizardForm>, <CreateDialog>, <EditDialog> & <ShowDialog> as well as a headless <Form> component.

Using Input Components

Wrapping form inputs with a <Controller> tag is a common pattern, so react-admin provides a shortcut for all the common input types: Input components. This means the BookEdit component doesn’t need to use react-hook-form’s <Controller> directly:

import * as React from "react";
import { useParams, useNavigate } from "react-router-dom";
-import { Controller } from "react-hook-form";
-import { useGetOne, useUpdate, Title, SimpleForm } from "react-admin";
+import { useGetOne, useUpdate, Title, SimpleForm, TextInput, SelectInput } from "react-admin";
-import { Card, TextField, MenuItem } from "@mui/material";
+import { Card } from "@mui/material";

export const BookEdit = () => {
  const { id } = useParams();
  const { isLoading, data } = useGetOne("books", { id });
  const [update, { isLoading: isSubmitting }] = useUpdate();
  const navigate = useNavigate();
  const onSubmit = (data) => {
    update(
        "books",
        { id, data },
        { onSuccess: () => { navigate('/books'); } }
    );
  };
  if (isLoading) return null;
  return (
    <div>
      <Title title="Book Edition" />
      <Card>
        <SimpleForm record={data} onSubmit={onSubmit}>
-         <Controller
-           name="title"
-           render={({ field }) => <TextField label="Title" {...field} />}
-         />
+         <TextInput source="title" />
-         <Controller
-           name="author"
-           render={({ field }) => <TextField label="Author" {...field} />}
-         />
+         <TextInput source="author" />
-         <Controller
-           name="availability"
-           render={({ field }) => (
-             <TextField select label="Availability" {...field}>
-               <MenuItem value="in_stock">In stock</MenuItem>
-               <MenuItem value="out_of_stock">Out of stock</MenuItem>
-               <MenuItem value="out_of_print">Out of print</MenuItem>
-             </TextField>
-           )}
-         />
+         <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>
      </Card>
    </div>
  );
};

<EditContext> Exposes Data And Callbacks

Instead of passing the record and onSubmit callback to the <SimpleForm> element, react-admin prefers putting them in an <EditContext> context. This allows any descendant element to “pull” the data and callback from the context.

import * as React from "react";
import { useParams, useNavigate } from "react-router-dom";
-import { useGetOne, useUpdate, Title, SimpleForm, TextInput, SelectInput } from "react-admin";
+import { useGetOne, useUpdate, Title, EditContextProvider, SimpleForm, TextInput, SelectInput } from "react-admin";
import { Card } from "@mui/material";

export const BookEdit = () => {
  const { id } = useParams();
  const { isLoading, data } = useGetOne("books", { id });
  const [update, { isLoading: isSubmitting }] = useUpdate();
  const navigate = useNavigate();
  const onSubmit = (data) => {
    update(
        "books",
        { id, data },
        { onSuccess: () => { navigate('/books'); } }
    );
  };
  if (isLoading) return null;
  return (
+   <EditContextProvider value={{
+     record: data,
+     isLoading,
+     save: onSubmit,
+     saving: isSubmitting,
+   }}>
      <div>
        <Title title="Book Edition" />
        <Card>
-         <SimpleForm record={data} onSubmit={onSubmit}>
+         <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>
        </Card>
      </div>
+   </EditContextProvider>
  );
};

Thanks to <EditContextProvider>, the <SimpleForm> component no longer needs explicit props. This may look a bit more verbose, but standardizing the EditContext value gives react-admin components a simplified API. And it enables further simplifications, explained below.

useEditController: The Controller Logic

The initial logic that grabs the id from the location, fetches the record from the API, and prepares the save callback is also common, and react-admin exposes the useEditController hook to do it:

import * as React from "react";
-import { useParams, useNavigate } from "react-router-dom";
-import { useGetOne, useUpdate, Title, EditContextProvider, SimpleForm, TextInput, SelectInput } from "react-admin";
+import { useEditController, Title, EditContextProvider, SimpleForm, TextInput, SelectInput } from "react-admin";
import { Card } from "@mui/material";

export const BookEdit = () => {
- const { id } = useParams();
- const { isLoading, data } = useGetOne("books", { id });
- const [update, { isLoading: isSubmitting }] = useUpdate();
- const navigate = useNavigate();
- const onSubmit = (data) => {
-   update(
-       "books",
-       { id, data },
-       { onSuccess: () => { navigate('/books'); } }
-   );
- };
+ const editContext = useEditController();
- if (isLoading) return null;
+ if (editContext.isLoading) return null;
  return (
-   <EditContextProvider value={{
-     record: data,
-     isLoading,
-     save: onSubmit,
-     saving: isSubmitting,
-   }}>
+   <EditContextProvider value={editContext}>
      <div>
        <Title title="Book Edition" />
        <Card>
          <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>
        </Card>
      </div>
    </EditContextProvider>
  );
};

Notice that useEditController doesn’t need the ‘books’ resource name - it relies on the ResourceContext, set by the <Resource> component, to guess it.

<EditBase>: Component Version Of The Controller

As calling the useEditController hook and putting its result into a context is also common, react-admin provides the <EditBase> component to do it. So the example can be further simplified to the following:

import * as React from "react";
-import { useEditController, Title, EditContextProvider, SimpleForm, TextInput, SelectInput } from "react-admin";
+import { EditBase, Title, SimpleForm, TextInput, SelectInput } from "react-admin";
import { Card } from "@mui/material";

export const BookEdit = () => {
- const editContext = useEditController();
- if (editContext.isLoading) return null;
  return (
-   <EditContextProvider value={editContext}>
+   <EditBase>
      <div>
        <Title title="Book Edition" />
        <Card>
          <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>
        </Card>
      </div>
-   </EditContextProvider>
+   </EditBase>
  );
};

<Edit> Renders Title, Fields, And Actions

<EditBase> is a headless component: it renders only its children. But almost every edition view needs a wrapping <div>, a title, and a <Card>. That’s why react-admin provides the <Edit> component, which includes the <EditBase> component, a title built from the resource name, and even a “Show” button if the resource has a show component:

import * as React from "react";
-import { EditBase, Title, SimpleForm, TextInput, SelectInput } from "react-admin";
+import { Edit, SimpleForm, TextInput, SelectInput } from "react-admin";

export const BookEdit = () => (
- <EditBase>
-   <div>
-     <Title title="Book Edition" />
-     <Card>
+ <Edit>
    <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>
-     </Card>
-   </div>
- </EditBase>
+ </Edit>
);

And that’s it! Now, the code is concise, expressive, and easier to maintain.

A Typical React-Admin Edit View

The react example had almost 60 lines of code, the react-admin one only has a quarter of that:

import * as React from "react";
import { Edit, SimpleForm, TextInput, SelectInput } from "react-admin";

export const BookEdit = () => (
  <Edit>
    <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>
);

React-admin components are not magic, they are React components designed to let you focus on the business logic and avoid repetitive tasks.

Tip: Actually, <Edit> does more than the code it replaces in the previous example: it handles notification and redirection upon submission, it sets the page title, and handles the error logic.

Setting Default Values

It’s the Form and Input component’s responsibility to define default values.

To define default values, you can add a defaultValues prop to form components (<SimpleForm>, <TabbedForm>, etc.), or add a defaultValue to individual input components. Let’s see each of these options.

Global Default Value

You can set the defaultValues at the form level. The expected value is an object, or a function returning an object, specifying default values for the created record. For instance:

const postDefaultValue = () => ({ id: uuid(), created_at: new Date(), nb_views: 0 });

export const PostCreate = () => (
    <Create>
        <SimpleForm defaultValues={postDefaultValue}>
            <TextInput source="title" />
            <RichTextInput source="body" />
            <NumberInput source="nb_views" />
        </SimpleForm>
    </Create>
);

Tip: You can include properties in the form defaultValues that are not listed as input components, like the created_at property in the previous example.

Per Input Default Value

Alternatively, you can specify a defaultValue prop directly in <Input> components. React-admin will ignore these default values if the Form already defines a global defaultValues (form > input):

export const PostCreate = () => (
    <Create>
        <SimpleForm>
            <TextInput source="title" />
            <RichTextInput source="body" />
            <NumberInput source="nb_views" defaultValue={0} />
        </SimpleForm>
    </Create>
);

Tip: Per-input default values cannot be functions. For default values computed at render time, set the defaultValues at the form level, as explained in the previous section.

Validating User Input

React-admin supports the most common validation strategies:

Validation example

Form validation deserves a section of its own ; check the Validation chapter for more details.

Altering the Form Values Before Submitting

Sometimes, you may want to alter the form values before sending them to the dataProvider. For those cases, use the transform prop either on the view component (<Create> or <Edit>) or on the <SaveButton> component.

In the following example, a create view for a Post displays a form with two submit buttons. Both buttons create a new record, but the ‘save and notify’ button should trigger an email to other admins on the server side. The POST /posts API route only sends the email when the request contains a special HTTP header.

So the save button with ‘save and notify’ will transform the record before react-admin calls the dataProvier.create() method, adding a notify field:

const PostCreateToolbar = props => (
    <Toolbar {...props}>
        <SaveButton />
        <SaveButton
            label="post.action.save_and_notify"
            transform={data => ({ ...data, notify: true })}
            type="button"
        />
    </Toolbar>
);

const PostCreate = () => (
    <Create>
        <SimpleForm toolbar={<PostCreateToolbar />}>
            // ...
        </SimpleForm>
    </Create>
);

Then, in the dataProvider.create() code, detect the presence of the notify field in the data, and add the HTTP header if necessary. Something like:

const dataProvider = {
    // ...
    create: (resource, params) => {
        const { notify, ...record } = params.data;
        const headers = new Headers({
            'Content-Type': 'application/json',
        });
        if (notify) {
            headers.set('X-Notify', 'true');
        }
        return httpClient(`${apiUrl}/${resource}`, {
            method: 'POST',
            body: JSON.stringify(record),
            headers,
        }).then(({ json }) => ({
            data: { ...record, id: json.id },
        }));
    },
}

Tip: <Edit>’s transform prop function also get the previousData in its second argument:

const PostEditToolbar = props => (
    <Toolbar {...props}>
        <SaveButton />
        <SaveButton
            label="post.action.save_and_notify"
            transform={(data, { previousData }) => ({
                ...data,
                avoidChangeField: previousData.avoidChangeField
            })}
            type="button"
        />
    </Toolbar>
);

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

Warning About Unsaved Changes

React-admin keeps track of the form state, so it can detect when the user leaves an Edit or Create page with unsaved changes. To avoid data loss, you can use this ability to ask the user to confirm before leaving a page with unsaved changes.

Warn About Unsaved Changes

Warning about unsaved changes is an opt-in feature: you must set the warnWhenUnsavedChanges prop in the form component to enable it:

export const TagEdit = () => (
    <Edit>
        <SimpleForm warnWhenUnsavedChanges>
            <TextField source="id" />
            <TextInput source="name" />
            ...
        </SimpleForm>
    </Edit>
);

And that’s all. warnWhenUnsavedChanges works for both <SimpleForm> and <TabbedForm>. In fact, this feature is provided by a custom hook called useWarnWhenUnsavedChanges(), which you can use in your own react-hook-form forms.

import { useForm } from 'react-hook-form';
import { useWarnWhenUnsavedChanges } from 'react-admin';

const MyForm = ({ onSubmit }) => {
    const form = useForm();
    return (
        <Form onSubmit={form.handleSubmit(onSubmit)} />
    );
}

const Form = ({ onSubmit }) => {
    // enable the warn when unsaved changes feature
    useWarnWhenUnsavedChanges(true);
    return (
        <form onSubmit={onSubmit}>
            <label id="firstname-label">First Name</label>
            <Field name="firstName" aria-labelledby="firstname-label" component="input" />
            <button type="submit">Submit</button>
        </form>
    );
};

Tip: You can customize the message displayed in the confirm dialog by setting the ra.message.unsaved_changes message in your i18nProvider.

Warning: This feature only works if you have a dependency on react-router 6.3.0 at most. The react-router team disabled this possibility in react-router 6.4, so warnWhenUnsavedChanges will silently fail with react-router 6.4 or later.

Submit On Enter

By default, pressing ENTER in any of the form inputs submits the form - this is the expected behavior in most cases. To disable the automated form submission on enter, set the type prop of the SaveButton component to button.

const MyToolbar = () => (
    <Toolbar>
        <SaveButton type="button" />
        <DeleteButton />
    </Toolbar>
);

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

However, some of your custom input components (e.g. Google Maps widget) may have special handlers for the ENTER key. In that case, you should prevent the default handling of the event on those inputs. This would allow other inputs to still submit the form on Enter:

export const PostEdit = () => (
    <Edit>
        <SimpleForm>
            <TextInput
                source="name"
                onKeyUp={event => {
                    if (event.key === 'Enter') {
                        event.stopPropagation();
                    }
                }}
            /> 
        </SimpleForm>
    </Edit>
);

Tip: <SaveButton type="button"> does not take into account a custom onSubmit prop passed to the enclosing <Form>. If you need to override the default submit callback for a <SaveButton type="button">, you should include an onClick prop in the button.

const MyToolbar = () => {
    const [update] = useUpdate();
    const { getValues } = useFormContext();
    const redirect = useRedirect();

    const handleClick = e => {
        e.preventDefault(); // necessary to prevent default SaveButton submit logic
        const { id, ...data } = getValues();
        update(
            'posts',
            { id, data },
            { onSuccess: () => { redirect('list'); }}
        );
    };

    return (
        <Toolbar>
            <SaveButton type="button" onClick={handleClick} />
            <DeleteButton />
        </Toolbar>
    );
};

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

AutoSave

In forms where users may spend a lot of time, it’s a good idea to save the form automatically after a few seconds of inactivity. You can auto save the form content by using the <AutoSave> component.

import { AutoSave } from '@react-admin/ra-form-layout';
import { Edit, SimpleForm, TextInput, DateInput, SelectInput, Toolbar } from 'react-admin';

const AutoSaveToolbar = () => (
    <Toolbar>
        <AutoSave />
    </Toolbar>
);

const PersonEdit = () => (
    <Edit mutationMode="optimistic">
        <SimpleForm
            resetOptions={{ keepDirtyValues: true }}
            toolbar={AutoSaveToolbar}
        >
            <TextInput source="first_name" />
            <TextInput source="last_name" />
            <DateInput source="dob" />
            <SelectInput source="sex" choices={[
                { id: 'male', name: 'Male' },
                { id: 'female', name: 'Female' },
            ]}/>
        </SimpleForm>
    </Edit>
);

Note that you must set the <SimpleForm resetOptions> prop to { keepDirtyValues: true }. If you forget that prop, any change entered by the end user after the autosave but before its acknowledgement by the server will be lost.

If you’re using it in an <Edit> page, you must also use a pessimistic or optimistic mutationMode - <AutoSave> doesn’t work with the default mutationMode="undoable".

Check the <AutoSave> component documentation for more details.

Adding Fields With Labels

All react-admin inputs handle the display of their label by wrapping their content inside a <Labeled> component.

You can wrap your own components inside the <Labeled> component too. You can either provide it the label prop directly or leverage the automatic label inference by providing it the source prop:

const IdentifierField = ({ label }) => {
    const record = useRecordContext();
    return (
        <Labeled label={label}>
            <Typography>{record.id}</Typography>
        </Labeled>
    );
};

// Here Labeled will try to translate the label with the translation key `resources.posts.fields.body`
// and with an inferred default of `Body`
const BodyField = () => {
    const record = useRecordContext();
    return (
        <Labeled source="body">
            <Typography>
                {record.body}
            </Typography>
        </Labeled>
    )
};

const PostEdit = () => (
    <Create>
        <SimpleForm>
            <IdentifierField label="Identifier" />
            <TextField source="title" />
            <BodyField />
            <NumberInput source="nb_views" />
        </SimpleForm>
    </Create>
);

Grouping Inputs

Sometimes, you may want to group inputs in order to make a form more approachable. You may use a <TabbedForm>, an <AccordionForm> or you may want to roll your own layout. In this case, you might need to know the state of a group of inputs: whether it’s valid or if the user has changed them (dirty/touched state).

For this, you can use the <FormGroupContextProvider>, which accepts a group name. All inputs rendered inside this context will register to it (thanks to the useInput hook). You may then call the useFormGroup hook to retrieve the status of the group. For example:

import { Edit, SimpleForm, TextInput, FormGroupContextProvider, useFormGroup, minLength } from 'react-admin';
import { Accordion, AccordionDetails, AccordionSummary, Typography } from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMoreIcon';

const PostEdit = () => (
    <Edit>
        <SimpleForm>
            <TextInput source="title" />
            <FormGroupContextProvider name="options">
                <Accordion>
                    <AccordionSummary
                        expandIcon={<ExpandMoreIcon />}
                        aria-controls="options-content"
                        id="options-header"
                    >
                        <AccordionSectionTitle name="options">
                            Options
                        </AccordionSectionTitle>
                    </AccordionSummary>
                    <AccordionDetails
                        id="options-content"
                        aria-labelledby="options-header"
                    >
                        <TextInput source="teaser" validate={minLength(20)} />
                    </AccordionDetails>
                </Accordion>
            </FormGroupContextProvider>
        </SimpleForm>
    </Edit>
);

const AccordionSectionTitle = ({ children, name }) => {
    const formGroupState = useFormGroup(name);

    return (
        <Typography color={
              !formGroupState.isValid && formGroupState.isDirty
                ? 'error'
                : 'inherit'
          }
        >
            {children}
        </Typography>
    );
};

Redirection After Submission

By default:

  • Submitting the form in the <Create> view redirects to the <Edit> view
  • Submitting the form in the <Edit> view redirects to the <List> view

You can customize the redirection by setting the redirect prop on the <Create> or <Edit> components. Possible values are “edit”, “show”, “list”, and false to disable redirection. You may also specify a custom path such as /my-custom-route. For instance, to redirect to the <Show> view after edition:

export const PostEdit = () => (
    <Edit redirect="show">
        <SimpleForm>
            ...
        </SimpleForm>
    </Edit>
);

You can also pass a custom route (e.g. “/home”) or a function as redirect prop value. For example, if you want to redirect to a page related to the current object:

// redirect to the related Author show page
const redirect = (resource, id, data) => `/author/${data.author_id}/show`;

export const PostEdit = () => (
    <Edit redirect={redirect}>
        <SimpleForm>
            // ...
        </SimpleForm>
    </Edit>
);

This affects both the submit button, and the form submission when the user presses ENTER in one of the form fields.

Tip: The redirect prop is ignored if you’ve set the onSuccess prop in the <Edit>/<Create> component, or in the <SaveButton> component.

Nested Forms

Users often need to edit data from several resources in the same form. React-admin doesn’t support nested forms, but provides ways to edit related data in a user-friendly way: