React Admin v4 Advanced Recipes: Creating a Record Related to the Current One

Matthieu Chaffotte
Matthieu ChaffotteOctober 12, 2023
#react#react-admin#tutorial

In this article, we'll see how to let users create a new record related to the current one (for example, to add a new review for the current product) in a react-admin application. The creation form should have its relationship already set, and it should redirect to the original page after saving.

This tutorial is an update over the same tutorial for react-admin v3, which was itself an update of the same tutorial for react-admin v2.

Prefilling A Creation Form

We'll leverage the <CreateButton> to link to the review creation page. We also need to preset the product_id field of the new review. We can use the state prop to do so. The <Create> component reads this state and uses it for the form default values.

// in src/products/CreateRelatedReviewButton.ts
import { CreateButton, useRecordContext } from 'react-admin';

export const CreateRelatedReviewButton = () => {
    const product = useRecordContext();
    return (
        <CreateButton
            resource="reviews"
            state={{ record: { product_id: product.id } }}
        />
    );
};

Add this new <CreateRelatedReviewButton> to the product edition form. In this example, we'll add it just after a <ReferenceManyField> that renders all the reviews for the current product:

// in src/products/ProductEdit.tsx
import {
    Edit,
    TabbedForm,
    ReferenceManyField,
    Datagrid,
    DateField,
    starField,
    EditButton,
} from 'react-admin';
import { StarRatingField } from './StarRatingField';
import { CreateRelatedReviewButton } from './CreateRelatedReviewButton';

const ProductEdit = () => (
    <Edit>
        <TabbedForm>
            // ...
            <TabbedForm.Tab>
                <ReferenceManyField reference="reviews" target="product_id">
                    <Datagrid>
                        <DateField source="date" />
                        <StarRatingField />
                        <TextField source="comment" />
                        <TextField source="status" />
                        <EditButton />
                    </Datagrid>
                </ReferenceManyField>
                <CreateRelatedReviewButton />
            </TabbedForm.Tab>
        </TabbedForm>
    </Edit>
);

Finally, we'll write a creation form for the review. It will be a standard react-admin creation pageā€”no need to set the form default values based on the location state, as <Create> does it automatically. We'll just override the success callback to redirect to the related product page after submission:

// in src/reviews/ReviewCreate.tsx
import {
    SimpleForm,
    Create,
    ReferenceInput,
    TextInput,
    DateInput,
    AutocompleteInput,
    required,
    useNotify,
    useRedirect,
    getRecordFromLocation,
} from 'react-admin';

const ReviewCreate = () => {
    const notify = useNotify();
    const redirect = useRedirect();
    const location = useLocation();

    const onSuccess = () => {
        // display a notification to confirm the creation
        notify('ra.notification.created');
        // get the initial values we set in the state earlier to know whether a product_id was provided
        const record = getRecordFromLocation(location);
        if (record && record.product_id) {
            // the record was created from the edit view of the product, redirect to it
            redirect(`/products/${record.product_id}/reviews`);
        } else {
            // redirect to the list of reviews
            redirect(`/reviews`);
        }
    };

    return (
        <Create mutationOptions={{ onSuccess }}>
            <SimpleForm>
                // ...
                <ReferenceInput source="product_id" reference="products">
                    <AutocompleteInput
                        optionText={productOptionRenderer}
                        validate={required()}
                    />
                </ReferenceInput>
                <DateInput
                    source="date"
                    defaultValue={new Date()}
                    validate={required()}
                />
                <StarRatingInput source="rating" defaultValue={2} />
                <TextInput
                    source="comment"
                    multiline
                    fullWidth
                    resettable
                    validate={required()}
                />
                // ...
            </SimpleForm>
        </Create>
    );
};

And that is it.

The previous code works fine, but the user experience is perfectible. Users have to leave the product page to go to the review creation page, only to come back to the product page on success. It would be better if the review creation form opened in a dialog, so that users would never need to leave the product page.

We can achieve that by replacing the <CreateButton> with a <CreateInDialogButton>: it opens a <Create> form in a <Dialog> without neither leaving the current view nor modifying the URL.

<CreateInDialogButton> expects a form as child. This means we don't need to use the location state to set the form default values. We can just use the record from the current context, leveraging the <WithRecord> component:

// in src/products/ProductEdit.tsx
const ProductEdit = () => (
    <Edit>
        <TabbedForm>
            // ...
            <TabbedForm.Tab>
                <ReferenceManyField reference="reviews" target="product_id">
                    <Datagrid>
                        <DateField source="date" />
                        <StarRatingField />
                        <TextField source="comment" />
                        <TextField source="status" />
                        <EditButton />
                    </Datagrid>
                </ReferenceManyField>
                <WithRecord
                    render={record => (
                        <CreateInDialogButton
                            record={{ product_id: record.id }}
                        >
                            <SimpleForm>
                                // ...
                                <DateInput
                                    source="date"
                                    defaultValue={new Date()}
                                    validate={required()}
                                />
                                <StarRatingInput
                                    source="rating"
                                    defaultValue={2}
                                />
                                <TextInput source="comment" multiline fullWidth resettable validate={required()} />
                            </SimpleForm>
                        </CreateInDialogButton>
                    )}
                />
            </TabbedForm.Tab>
        </TabbedForm>
    </Edit>
);

Since the dialog closes after a successful form submission, we don't need to customize the redirection to the product edition view anymore. Also, as the creation form will never be displayed directly, we don't even need to include a <ReferenceInput> for the product_id. This solution requires less code and provides a better user experience than the first one.

Note that <CreateInDialogButton> is only a good fit for small forms, that can fit in a dialog. Also, it's an Enterprise Edition component.

Conclusion

We hope this recipe will speed up your react-admin development and improve the user experience of your apps.

You can test both approaches and compare the the code in the react-admin e-commerce demo:

Did you like this article? Share it!