Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for mutationMode in useCreate #10530

Open
wants to merge 6 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/Create.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ You can customize the `<Create>` component using the following props:
* `className`: passed to the root component
* [`component`](#component): override the root component
* [`disableAuthentication`](#disableauthentication): disable the authentication check
* [`mutationMode`](#mutationmode): switch to optimistic or undoable mutations (pessimistic by default)
* [`mutationOptions`](#mutationoptions): options for the `dataProvider.create()` call
* [`record`](#record): initialize the form with a record
* [`redirect`](#redirect): change the redirect location after successful creation
Expand Down Expand Up @@ -154,6 +155,34 @@ const PostCreate = () => (
);
```

## `mutationMode`

The `<Create>` view exposes a Save button, which perform a "mutation" (i.e. it creates the data). React-admin offers three modes for mutations. The mode determines when the side effects (redirection, notifications, etc.) are executed:

- `pessimistic` (default): 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`: 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 `<Create>` use the `pessimistic` mutation mode as the new record identifier is often generated on the backend. However, should you decide to generate this identifier client side, you can change the `mutationMode` to either `optimistic` or `undoable`:

```jsx
const PostCreate = () => (
<Create mutationMode="optimistic" transform={data => ({ id: generateId(), ...data })}>
// ...
</Create>
);
```

And to make the record creation undoable:

```jsx
const PostCreate = () => (
<Create mutationMode="undoable" transform={data => ({ id: generateId(), ...data })}>
// ...
</Create>
);
```

## `mutationOptions`

You can customize the options you pass to react-query's `useMutation` hook, e.g. to pass [a custom `meta`](./Actions.md#meta-parameter) to the `dataProvider.create()` call.
Expand Down
1 change: 1 addition & 0 deletions docs/CreateBase.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ You can customize the `<CreateBase>` component using the following props, docume

* `children`: the components that renders the form
* [`disableAuthentication`](./Create.md#disableauthentication): disable the authentication check
* [`mutationMode`](./Create.md#mutationmode): Switch to optimistic or undoable mutations (pessimistic by default)
* [`mutationOptions`](./Create.md#mutationoptions): options for the `dataProvider.create()` call
* [`record`](./Create.md#record): initialize the form with a record
* [`redirect`](./Create.md#redirect): change the redirect location after successful creation
Expand Down
267 changes: 264 additions & 3 deletions docs/useCreate.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This hook allows to call `dataProvider.create()` when the callback is executed.

## Syntax

```jsx
```tsx
const [create, { data, isPending, error }] = useCreate(
resource,
{ data, meta },
Expand All @@ -19,7 +19,7 @@ const [create, { data, isPending, error }] = useCreate(

The `create()` method can be called with the same parameters as the hook:

```jsx
```tsx
create(
resource,
{ data },
Expand All @@ -31,7 +31,7 @@ So, should you pass the parameters when calling the hook, or when executing the

## Usage

```jsx
```tsx
// set params when calling the hook
import { useCreate, useRecordContext } from 'react-admin';

Expand Down Expand Up @@ -61,6 +61,267 @@ const LikeButton = () => {
};
```

## Params

The second argument of the `useCreate` hook is an object with the following properties:

- `data`: the new data for the record,
- `meta`: an object to pass additional information to the dataProvider (optional).

```tsx
const LikeButton = () => {
const record = useRecordContext();
const like = { postId: record.id };
const [create, { isPending, error }] = useCreate('likes', { data: like });
const handleClick = () => {
create()
}
if (error) { return <p>ERROR</p>; }
return <button disabled={isPending} onClick={handleClick}>Like</button>;
};```

`data` the record to create.

`meta` is helpful for passing additional information to the dataProvider. For instance, you can pass the current user to let a server-side audit system know who made the creation.

## Options

`useCreate`'s third parameter is an `options` object with the following properties:

- `mutationMode`,
- `onError`,
- `onSettled`,
- `onSuccess`,
- `returnPromise`.

```tsx
const notify = useNotify();
const redirect = useRedirect();
const like = { postId: record.id };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nipitck: the like should have an id if you want to use mutationMode: 'optimistic'

const [create, { isPending, error }] = useCreate(
'likes',
{ data: like },
{
mutationMode: 'optimistic',
onSuccess: () => {
notify('Like created');
redirect('/reviews');
},
onError: (error) => {
notify(`Like creation error: ${error.message}`, { type: 'error' });
},
});

```

Additional options are passed to [React Query](https://tanstack.com/query/v5/)'s [`useMutation`](https://tanstack.com/query/v5/docs/react/reference/useMutation) hook. This includes:

- `gcTime`,
- `networkMode`,
- `onMutate`,
- `retry`,
- `retryDelay`,
- `mutationKey`,
- `throwOnError`.

Check [the useMutation documentation](https://tanstack.com/query/v5/docs/react/reference/useMutation) for a detailed description of all options.

**Tip**: In react-admin components that use `useCreate`, you can override the mutation options using the `mutationOptions` prop. This is very common when using mutation hooks like `useCreate`, e.g., to display a notification or redirect to another page.

For instance, here is a button using `<Create mutationOptions>` to notify the user of success using the bottom notification banner:

{% raw %}
```tsx
import * as React from 'react';
import { useNotify, useRedirect, Create, SimpleForm } from 'react-admin';

const PostCreate = () => {
const notify = useNotify();
const redirect = useRedirect();

const onSuccess = (data) => {
notify(`Changes saved`);
redirect(`/posts/${data.id}`);
};

return (
<Create mutationOptions={{ onSuccess }}>
<SimpleForm>
...
</SimpleForm>
</Create>
);
}
```
{% endraw %}

## Return Value

The `useCreate` hook returns an array with two values:

- the `create` callback, and
- a mutation state object with the following properties:
- `data`,
- `error`,
- `isError`,
- `isIdle`,
- `isPending`,
- `isPaused`,
- `isSuccess`,
- `failureCount`,
- `failureReason`,
- `mutate`,
- `mutateAsync`,
- `reset`,
- `status`,
- `submittedAt`,
- `variables`.

The `create` callback can be called with a `resource` and a `param` argument, or, if these arguments were defined when calling `useCreate`, with no argument at all:

```jsx
// Option 1: define the resource and params when calling the callback
const [create, { isPending }] = useCreate();
const handleClick = () => {
create(resource, params, options);
};

// Option 2: define the resource and params when calling the hook
const [create, { isPending }] = useCreate(resource, params, options);
const handleClick = () => {
create();
};
```

For a detailed description of the mutation state, check React-query's [`useMutation` documentation](https://tanstack.com/query/v5/docs/react/reference/useMutation).

Since `useCreate` is mainly used in event handlers, success and error side effects are usually handled in the `onSuccess` and `onError` callbacks. In most cases, the mutation state is just used to disable the save button while the mutation is pending.

## `mutationMode`

The `mutationMode` option lets you switch between three rendering modes, which change how the success side effects are triggered:

- `pessimistic` (the default)
- `optimistic`, and
- `undoable`

**Note**: For `optimistic` and `undoable` modes, the record `id` must be generated client side. Those two modes are useful when building local first applications.

Here is an example of using the `optimistic` mode:

```jsx
// In optimistic mode, ids must be generated client side
const id = uuid.v4();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering if it was common to do it like this, generating the id client side. I understand the why, but I'm not sure on the how we solve this. Couldn't we have a mechanism witch a temporary id, replacing it as the data is fetched ?

Copy link
Collaborator Author

@djhi djhi Feb 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it is common in offline/local first scenarios. A temporary mechanism would make handling relationships such as those with ReferenceOneInput or ReferenceManyToManyInput a nightmare. This is a good starting point and if we do find a way to use temporary id, we can add it later.

const [create, { data, isPending, error }] = useCreate(
'comments',
{ data: { id, message: 'Lorem ipsum' } },
{
mutationMode: 'optimistic',
onSuccess: () => { /* ... */},
onError: () => { /* ... */},
}
);
```

In `pessimistic` mode, the `onSuccess` side effect executes *after* the dataProvider responds.

In `optimistic` mode, the `onSuccess` side effect executes just before the `dataProvider.create()` is called, without waiting for the response.

In `undoable` mode, the `onSuccess` side effect fires immediately. The actual call to the dataProvider is delayed until the create notification hides. If the user clicks the undo button, the `dataProvider.create()` call is never made.

See [Optimistic Rendering and Undo](./Actions.md#optimistic-rendering-and-undo) for more details.

**Tip**: If you need a side effect to be triggered after the dataProvider response in `optimistic` and `undoable` modes, use the `onSettled` callback.

## `onError`

The `onError` callback is called when the mutation fails. It's the perfect place to display an error message to the user.

```jsx
const notify = useNotify();
const [create, { data, isPending, error }] = useCreate(
'comments',
{ id: record.id, data: { isApproved: true } },
{
onError: () => {
notify('Error: comment not approved', { type: 'error' });
},
}
);
```

**Note**: If you use the `retry` option, the `onError` callback is called only after the last retry has failed.

## `onSettled`

The `onSettled` callback is called at the end of the mutation, whether it succeeds or fails. It will receive either the `data` or the `error`.

```jsx
const notify = useNotify();
const [create, { data, isPending, error }] = useCreate(
'comments',
{ id: record.id, data: { isApproved: true } },
{
onSettled: (data, error) => {
// ...
},
}
);
```

**Tip**: The `onSettled` callback is perfect for calling a success side effect after the dataProvider response in `optimistic` and `undoable` modes.

## `onSuccess`

The `onSuccess` callback is called when the mutation succeeds. It's the perfect place to display a notification or to redirect the user to another page.

```jsx
const notify = useNotify();
const redirect = useRedirect();
const [create, { data, isPending, error }] = useCreate(
'comments',
{ id: record.id, data: { isApproved: true } },
{
onSuccess: () => {
notify('Comment approved');
redirect('/comments');
},
}
);
```

In `pessimistic` mutation mode, `onSuccess` executes *after* the `dataProvider.create()` responds. React-admin passes the result of the `dataProvider.create()` call as the first argument to the `onSuccess` callback.

In `optimistic` mutation mode, `onSuccess` executes *before* the `dataProvider.create()` is called, without waiting for the response. The callback receives no argument.

In `undoable` mutation mode, `onSuccess` executes *before* the `dataProvider.create()` is called. The actual call to the dataProvider is delayed until the create notification hides. If the user clicks the undo button, the `dataProvider.create()` call is never made. The callback receives no argument.

## `returnPromise`

By default, the `create` callback that `useCreate` returns is synchronous and returns nothing. To execute a side effect after the mutation has succeeded, you can use the `onSuccess` callback.

If this is not enough, you can use the `returnPromise` option so that the `create` callback returns a promise that resolves when the mutation has succeeded and rejects when the mutation has failed.

This can be useful if the server changes the record, and you need the newly created data to create/update another record.

```jsx
const [create] = useCreate(
'posts',
{ id: record.id, data: { isPublished: true } },
{ returnPromise: true }
);
const [create] = useCreate('auditLogs');
Comment on lines +308 to +313
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't declare two variables called create 🤣


const createPost = async () => {
try {
const post = await create();
create('auditLogs', { data: { action: 'create', recordId: post.id, date: post.createdAt } });
} catch (error) {
// handle error
}
};
```

## TypeScript

The `useCreate` hook accepts a generic parameter for the record type and another for the error type:
Expand Down
2 changes: 2 additions & 0 deletions docs/useCreateController.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const BookCreate = () => {
`useCreateController` accepts an object with the following keys, all optional:

* [`disableAuthentication`](./Create.md#disableauthentication): Disable the authentication check
* [`mutationMode`](./Create.md#mutationmode): Switch to optimistic or undoable mutations (pessimistic by default)
* [`mutationOptions`](./Create.md#mutationoptions): Options for the `dataProvider.create()` call
* [`record`](./Create.md#record): Use the provided record as base instead of fetching it
* [`redirect`](./Create.md#redirect): Change the redirect location after successful creation
Expand All @@ -65,6 +66,7 @@ These fields are documented in [the `<Create>` component](./Create.md) documenta
```jsx
const {
defaultTitle, // Translated title based on the resource, e.g. 'Create New Post'
mutationMode, // Mutation mode argument passed as parameter, or 'pessimistic' if not defined
record, // Default values of the creation form
redirect, // Default redirect route. Defaults to 'list'
resource, // Resource name, deduced from the location. e.g. 'posts'
Expand Down
8 changes: 4 additions & 4 deletions packages/ra-core/src/controller/create/CreateBase.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe('CreateBase', () => {
test: 'test',
},
{ data: { test: 'test' }, resource: 'posts' },
undefined
{ snapshot: [] }
);
});
});
Expand Down Expand Up @@ -85,7 +85,7 @@ describe('CreateBase', () => {
test: 'test',
},
{ data: { test: 'test' }, resource: 'posts' },
undefined
{ snapshot: [] }
);
});
expect(onSuccess).not.toHaveBeenCalled();
Expand All @@ -112,7 +112,7 @@ describe('CreateBase', () => {
expect(onError).toHaveBeenCalledWith(
{ message: 'test' },
{ data: { test: 'test' }, resource: 'posts' },
undefined
{ snapshot: [] }
);
});
});
Expand All @@ -139,7 +139,7 @@ describe('CreateBase', () => {
expect(onErrorOverride).toHaveBeenCalledWith(
{ message: 'test' },
{ data: { test: 'test' }, resource: 'posts' },
undefined
{ snapshot: [] }
);
});
expect(onError).not.toHaveBeenCalled();
Expand Down
Loading
Loading