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

Update useUpdate to use react-query #6891

Merged
merged 25 commits into from
Nov 25, 2021
Merged
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
229 changes: 204 additions & 25 deletions UPGRADE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,209 @@
# Upgrade to 4.0

## Changed Signature Of Data Provider Hooks

Specialized data provider hooks (like `useUpdate`) have a new signature.

For mutations:

```diff
-const [update, { loading }] = useUpdate(
- 'posts',
- 123,
- { likes: 12 },
- { id: 123, title: "hello, world", likes: 122 }
-);
+const [update, { isLoading }] = useUpdate(
+ 'posts',
+ {
+ id: 123,
+ data: { likes: 12 },
+ previousData: { id: 123, title: "hello, world", likes: 122 }
+ }
+);
```

There are 2 changes:

- `loading` is renamed to `isLoading`
- the hook signature now reflects the dataProvider signature (so every hook now takes 2 arguments, `resource` and `params`).

The signature of the `update` mutation callback has also changed, and is the same as the hook:

```diff
-update(resource, id, data, previousData, options);
+update(resource, { id, data, previousData }, options);
```

This new signature should be easier to learn and use.

To upgrade, check every instance of your code of the following hooks:

- `useUpdate`

And update the calls. If you're using TypeScript, your code won't compile until you properly upgrade the calls.

These hooks are now powered by react-query, so the state argument contains way more than just `isLoading` (`reset`, `status`, `refetch`, etc.). Check the [`useQuery`](https://react-query.tanstack.com/reference/useQuery) and the [`useMutation`](https://react-query.tanstack.com/reference/useMutation) documentation on the react-query website for more details.

## Mutation Callbacks Can No Longer Be Used As Event Handlers

In 3.0, you could use a mutation callback in an event handler, e.g. a click handler on a button. This is no longer possible, so you'll have to call the callback manually inside a handler function:

```diff
const IncreaseLikeButton = ({ record }) => {
const diff = { likes: record.likes + 1 };
const [update, { isLoading, error }] = useUpdate('likes', { id: record.id, data: diff, previousData: record });
if (error) { return <p>ERROR</p>; }
- return <button disabled={isLoading} onClick={update}>Like</button>;
+ return <button disabled={isLoading} onClick={() => update()}>Like</button>;
};
```

TypeScript will complain if you don't.

Note that your code will be more readable if you pass the mutation parameters to the mutation callback instaed of the mutation hook, e.g.

```diff
const IncreaseLikeButton = ({ record }) => {
const diff = { likes: record.likes + 1 };
- const [update, { isLoading, error }] = useUpdate('likes', { id: record.id, data: diff, previousData: record });
+ const [update, { isLoading, error }] = useUpdate();
+ const handleClick = () => {
+ update('likes', { id: record.id, data: diff, previousData: record });
+ };
if (error) { return <p>ERROR</p>; }
- return <button disabled={isLoading} onClick={update}>Like</button>;
+ return <button disabled={isLoading} onClick={handleClick}>Like</button>;
};
```

## `onSuccess` And `onFailure` Props Have Moved

If you need to override the success or failure side effects of a component, you now have to use the `queryOptions` (for query side effects) or `mutationOptions` (for mutation side effects).

For instance, here is how to override the side eggects for the `getOne` query in a `<Show>` component:

```diff
const PostShow = () => {
const onSuccess = () => {
// do something
};
const onFailure = () => {
// do something
};
return (
<Show
- onSuccess={onSuccess}
- onFailure={onFailure}
+ queryOptions={{
+ onSuccess: onSuccess,
+ onError: onFailure
+ }}
>
<SimpleShowLayout>
<TextField source="title" />
</SimpleShowLayout>
</Show>
);
};
```

Here is how to customize side effects on the `update` mutation in `<Edit>`:

```diff
const PostEdit = () => {
const onSuccess = () => {
// do something
};
const onFailure = () => {
// do something
};
return (
<Edit
- onSuccess={onSuccess}
- onFailure={onFailure}
+ mutationOptions={{
+ onSuccess: onSuccess,
+ onError: onFailure
+ }}
>
<SimpleForm>
<TextInput source="title" />
</SimpleForm>
</Show>
);
};
```

Note that the `onFailure` prop was renamed to `onError` in the options, to match the react-query convention.

**Tip**: `<Edit>` also has a `queryOption` prop allowing you to specify custom success and error side effects for the `getOne` query.

The change concerns the following components:

- `useEditController`
- `<Edit>`
- `<EditBase>`
- `<SaveButton>`
- `useShowController`
- `<Show>`
- `<ShowBase>`

## `onSuccess` Callback On DataProvider Hooks And Components Has A New Signature

The `onSuccess` callback used to receive the *response* from the dataProvider. On specialized hooks, it now receives the `data` property of the response instead.

```diff
const [update] = useUpdate();
const handleClick = () => {
update(
'posts',
{ id: 123, data: { likes: 12 } },
{
- onSuccess: ({ data }) => {
+ onSuccess: (data) => {
// do something with data
}
}
);
};
```

The change concerns the following components:

- `useUpdate`
- `useEditController`
- `<Edit>`
- `<EditBase>`
- `<SaveButton>`
- `useGetOne`
- `useShowController`
- `<Show>`
- `<ShowBase>`

## `<Edit successMessage>` Prop Was Removed

This prop has been deprecated for a long time. Replace it with a custom success handler in the `mutationOptions`:

```diff
-import { Edit, SimpleForm } from 'react-admin';
+import { Edit, SimpleForm, useNotify } from 'react-admin';

const PostEdit = () => {
+ const notify = useNotify();
+ const onSuccess = () => notify('Post updated successfully');
return (
- <Edit successMessage="Post updated successfully">
+ <Edit mutationOptions={{ onSuccess }}>
<SimpleForm>
...
</SimpleForm>
</Edit>
);
};
```


## No More Prop Injection In Page Components

Page components (`<List>`, `<Show>`, etc.) used to expect to receive props (route parameters, permissions, resource name). These components don't receive any props anymore by default. They use hooks to get the props they need from contexts or route state.
Expand Down Expand Up @@ -291,31 +495,6 @@ const BookDetail = ({ id }) => {

The new props are actually returned by react-query's `useQuery` hook. Check [their documentation](https://react-query.tanstack.com/reference/useQuery) for more information.

## Page Components No Longer Accept `onSuccess` and `onFailure` Props

Prior to 4.0, the page components (`<List>`, `<Show>`, etc.) used to accept props for success and failure side effects. They now accept a generic `queryOptions` prop, which is passed to the underlying react-query `useQuery` call.

This options object accepts `onSuccess` and `onError` fields, so you can migrate your code as follows:

```diff
const PostShow = () => {
const onSuccess = () => {
// do something
};
const onFailure = () => {
// do something
};
return (
- <Show {...props} onSuccess={onSuccess} onFailure={onFailure}>
+ <Show {...props} queryOptions={{ onSuccess: onSuccess, onError: onFailure }}>
<SimpleShowLayout>
<TextField source="title" />
</SimpleShowLayout>
</Show>
);
};
```

## Unit Tests for Data Provider Dependent Components Need A QueryClientContext

If you were using components dependent on the dataProvider hooks in isolation (e.g. in unit or integration tests), you now need to wrap them inside a `<QueryClientContext>` component, to let the access react-query's `QueryClient` instance.
Expand Down
5 changes: 4 additions & 1 deletion cypress/integration/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ describe('Edit Page', () => {
cy.contains('Tech').click();
cy.get('li[aria-label="Clear value"]').click();
EditPostPage.submit();
ListPagePosts.waitUntilDataLoaded();

EditPostPage.navigate();
EditPostPage.gotoTab(3);
Expand All @@ -250,13 +251,15 @@ describe('Edit Page', () => {
);
});

it('should refresh the list when the update fails', () => {
// FIXME unskip me when useGetList uses the react-query API
it.skip('should refresh the list when the update fails', () => {
ListPagePosts.navigate();
ListPagePosts.nextPage(); // Ensure the record is visible in the table

EditPostPage.navigate();
EditPostPage.setInputValue('input', 'title', 'f00bar');
EditPostPage.submit();
ListPagePosts.waitUntilDataLoaded();

cy.get(ListPagePosts.elements.recordRows)
.eq(2)
Expand Down
48 changes: 24 additions & 24 deletions docs/Actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,8 @@ import * as React from "react";
import { useUpdate, Button } from 'react-admin';

const ApproveButton = ({ record }) => {
const [approve, { loading }] = useUpdate('comments', record.id, { isApproved: true }, record);
return <Button label="Approve" onClick={approve} disabled={loading} />;
const [approve, { isLoading }] = useUpdate('comments', { id: record.id, data: { isApproved: true }, previousData: record });
return <Button label="Approve" onClick={approve} disabled={isLoading} />;
};
```

Expand Down Expand Up @@ -422,44 +422,47 @@ const LikeButton = ({ record }) => {
const like = { postId: record.id };
const [create, { loading, error }] = useCreate('likes', like);
if (error) { return <p>ERROR</p>; }
return <button disabled={loading} onClick={create}>Like</button>;
return <button disabled={loading} onClick={() => create()}>Like</button>;
};
```

### `useUpdate`

```jsx
// syntax
const [update, { data, loading, loaded, error }] = useUpdate(resource, id, data, previousData, options);
const [update, { data, isLoading, error }] = useUpdate(resource, { id, data, previousData }, options);
```

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

```jsx
update(resource, { id, data, previousData }, options);
```

The `update()` method can be called in 3 different ways:
- with the same parameters as the `useUpdate()` hook: `update(resource, id, data, previousData, options)`
- with the same syntax as `useMutation`: `update({ resource, payload: { id, data, previousData } }, options)`
- with no parameter (if they were already passed to useUpdate()): `update()`
This means the parameters can be passed either when calling the hook, or when calling the callback.

```jsx
// set params when calling the update callback
import { useUpdate } from 'react-admin';

const IncreaseLikeButton = ({ record }) => {
const diff = { likes: record.likes + 1 };
const [update, { loading, error }] = useUpdate();
const [update, { isLoading, error }] = useUpdate();
const handleClick = () => {
update('likes', record.id, diff, record)
update('likes', { id: record.id, data: diff, previousData: record })
}
if (error) { return <p>ERROR</p>; }
return <button disabled={loading} onClick={handleClick}>Like</button>;
return <button disabled={isLoading} onClick={handleClick}>Like</button>;
};

// or set params when calling the hook
import { useUpdate } from 'react-admin';

const IncreaseLikeButton = ({ record }) => {
const diff = { likes: record.likes + 1 };
const [update, { loading, error }] = useUpdate('likes', record.id, diff, record);
const [update, { isLoading, error }] = useUpdate('likes', { id: record.id, data: diff, previousData: record });
if (error) { return <p>ERROR</p>; }
return <button disabled={loading} onClick={update}>Like</button>;
return <button disabled={isLoading} onClick={update}>Like</button>;
};
```

Expand Down Expand Up @@ -884,21 +887,19 @@ import { useUpdate, useNotify, useRedirect, Button } from 'react-admin';
const ApproveButton = ({ record }) => {
const notify = useNotify();
const redirect = useRedirect();
const [approve, { loading }] = useUpdate(
const [approve, { isLoading }] = useUpdate(
'comments',
record.id,
{ isApproved: true },
record,
{ id: record.id, data: { isApproved: true }, previousData: record },
{
mutationMode: 'undoable',
onSuccess: () => {
redirect('/comments');
notify('Comment approved', { undoable: true });
},
onFailure: (error) => notify(`Error: ${error.message}`, { type: 'warning' }),
onError: (error) => notify(`Error: ${error.message}`, { type: 'warning' }),
}
);
return <Button label="Approve" onClick={approve} disabled={loading} />;
return <Button label="Approve" onClick={approve} disabled={isLoading} />;
};
```

Expand All @@ -919,21 +920,20 @@ import { useUpdate, useNotify, useRedirect, Button } from 'react-admin';
const ApproveButton = ({ record }) => {
const notify = useNotify();
const redirect = useRedirect();
const [approve, { loading }] = useUpdate(
const [approve, { isLoading }] = useUpdate(
'comments',
record.id,
{ isApproved: true },
{ id: record.id, data: { isApproved: true } },
{
+ action: 'MY_CUSTOM_ACTION',
mutationMode: 'undoable',
onSuccess: ({ data }) => {
redirect('/comments');
notify('Comment approved', { undoable: true });
},
onFailure: (error) => notify(`Error: ${error.message}`, { type: 'warning' }),
onError: (error) => notify(`Error: ${error.message}`, { type: 'warning' }),
}
);
return <Button label="Approve" onClick={approve} disabled={loading} />;
return <Button label="Approve" onClick={approve} disabled={isLoading} />;
};
```

Expand Down
Loading