React Admin v4 Advanced Recipes: Creating a Record Related to the Current One
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.
Creating a Related Record In a Dialog
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: