Skip to content

Commit 77d1756

Browse files
authored
Merge pull request #11017 from marmelab/fix-optimistic-query-invalidation-2
Fix optimistic query invalidation and avoid invalidating the same query twice
2 parents 12f822f + 9a50986 commit 77d1756

17 files changed

+616
-136
lines changed

packages/ra-core/src/dataProvider/useCreate.spec.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@ import {
2626
WithMiddlewaresSuccess as WithMiddlewaresSuccessUndoable,
2727
WithMiddlewaresError as WithMiddlewaresErrorUndoable,
2828
} from './useCreate.undoable.stories';
29-
import { Middleware, MutationMode, Params } from './useCreate.stories';
29+
import {
30+
Middleware,
31+
MutationMode,
32+
Params,
33+
InvalidateList,
34+
} from './useCreate.stories';
3035

3136
describe('useCreate', () => {
3237
it('returns a callback that can be used with create arguments', async () => {
@@ -627,4 +632,15 @@ describe('useCreate', () => {
627632
await screen.findByText('Bazinga');
628633
});
629634
});
635+
636+
it('invalidates getList query when dataProvider resolves in undoable mode', async () => {
637+
render(<InvalidateList mutationMode="undoable" />);
638+
fireEvent.change(await screen.findByLabelText('title'), {
639+
target: { value: 'New Post' },
640+
});
641+
fireEvent.click(screen.getByText('Save'));
642+
await screen.findByText('resources.posts.notifications.created');
643+
fireEvent.click(screen.getByText('Close'));
644+
await screen.findByText('3: New Post');
645+
});
630646
});

packages/ra-core/src/dataProvider/useCreate.stories.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as React from 'react';
22
import { QueryClient, useIsMutating } from '@tanstack/react-query';
3+
import fakeRestDataProvider from 'ra-data-fakerest';
34

45
import { CoreAdmin, CoreAdminContext, Resource } from '../core';
56
import { useCreate } from './useCreate';
@@ -383,3 +384,70 @@ const RefreshButton = () => {
383384
</button>
384385
);
385386
};
387+
388+
export const InvalidateList = ({
389+
mutationMode,
390+
}: {
391+
mutationMode: MutationModeType;
392+
}) => {
393+
const dataProvider = fakeRestDataProvider(
394+
{
395+
posts: [
396+
{ id: 1, title: 'Hello' },
397+
{ id: 2, title: 'World' },
398+
],
399+
},
400+
process.env.NODE_ENV !== 'test',
401+
process.env.NODE_ENV === 'test' ? 10 : 1000
402+
);
403+
404+
return (
405+
<TestMemoryRouter initialEntries={['/posts/create']}>
406+
<CoreAdmin dataProvider={dataProvider}>
407+
<Resource
408+
name="posts"
409+
create={
410+
<CreateBase mutationMode={mutationMode}>
411+
<Form>
412+
{mutationMode !== 'pessimistic' && (
413+
<TextInput source="id" defaultValue={3} />
414+
)}
415+
<TextInput source="title" />
416+
<button type="submit">Save</button>
417+
</Form>
418+
</CreateBase>
419+
}
420+
list={
421+
<ListBase loading={<p>Loading...</p>}>
422+
<RecordsIterator
423+
render={(record: any) => (
424+
<div
425+
style={{
426+
display: 'flex',
427+
gap: '8px',
428+
alignItems: 'center',
429+
}}
430+
>
431+
{record.id}: {record.title}
432+
</div>
433+
)}
434+
/>
435+
<Notification />
436+
</ListBase>
437+
}
438+
/>
439+
</CoreAdmin>
440+
</TestMemoryRouter>
441+
);
442+
};
443+
InvalidateList.args = {
444+
mutationMode: 'undoable',
445+
};
446+
InvalidateList.argTypes = {
447+
mutationMode: {
448+
control: {
449+
type: 'select',
450+
},
451+
options: ['pessimistic', 'optimistic', 'undoable'],
452+
},
453+
};

packages/ra-core/src/dataProvider/useCreate.ts

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ export const useCreate = <
160160

161161
return clonedData;
162162
},
163-
getSnapshot: ({ resource, ...params }, { mutationMode }) => {
163+
getQueryKeys: ({ resource, ...params }, { mutationMode }) => {
164164
const queryKeys: any[] = [
165165
[resource, 'getList'],
166166
[resource, 'getInfiniteList'],
@@ -176,27 +176,7 @@ export const useCreate = <
176176
]);
177177
}
178178

179-
/**
180-
* Snapshot the previous values via queryClient.getQueriesData()
181-
*
182-
* The snapshotData ref will contain an array of tuples [query key, associated data]
183-
*
184-
* @example
185-
* [
186-
* [['posts', 'getOne', { id: '1' }], { id: 1, title: 'Hello' }],
187-
* [['posts', 'getList'], { data: [{ id: 1, title: 'Hello' }], total: 1 }],
188-
* [['posts', 'getMany'], [{ id: 1, title: 'Hello' }]],
189-
* ]
190-
*
191-
* @see https://react-query-v3.tanstack.com/reference/QueryClient#queryclientgetqueriesdata
192-
*/
193-
const snapshot = queryKeys.reduce(
194-
(prev, queryKey) =>
195-
prev.concat(queryClient.getQueriesData({ queryKey })),
196-
[] as Snapshot
197-
);
198-
199-
return snapshot;
179+
return queryKeys;
200180
},
201181
getMutateWithMiddlewares: mutationFn => {
202182
if (getMutateWithMiddlewares) {

packages/ra-core/src/dataProvider/useDelete.spec.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
ErrorCase as ErrorCaseUndoable,
2121
SuccessCase as SuccessCaseUndoable,
2222
} from './useDelete.undoable.stories';
23-
import { MutationMode, Params } from './useDelete.stories';
23+
import { MutationMode, Params, InvalidateList } from './useDelete.stories';
2424

2525
describe('useDelete', () => {
2626
it('returns a callback that can be used with deleteOne arguments', async () => {
@@ -732,5 +732,16 @@ describe('useDelete', () => {
732732
});
733733
});
734734
});
735+
736+
it('invalidates getList query when dataProvider resolves in undoable mode', async () => {
737+
render(<InvalidateList mutationMode="undoable" />);
738+
await screen.findByText('Title: Hello');
739+
fireEvent.click(await screen.findByText('Delete'));
740+
await screen.findByText('resources.posts.notifications.deleted');
741+
fireEvent.click(screen.getByText('Close'));
742+
await waitFor(() => {
743+
expect(screen.queryByText('1: Hello')).toBeNull();
744+
});
745+
});
735746
});
736747
});

packages/ra-core/src/dataProvider/useDelete.stories.tsx

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
import * as React from 'react';
22
import { useState } from 'react';
33
import { QueryClient, useIsMutating } from '@tanstack/react-query';
4+
import fakeRestDataProvider from 'ra-data-fakerest';
45

5-
import { CoreAdminContext } from '../core';
6+
import { CoreAdmin, CoreAdminContext, Resource } from '../core';
67
import { useDelete } from './useDelete';
78
import { useGetList } from './useGetList';
89
import type { DataProvider, MutationMode as MutationModeType } from '../types';
10+
import {
11+
EditBase,
12+
ListBase,
13+
RecordsIterator,
14+
useDeleteController,
15+
WithRecord,
16+
} from '../controller';
17+
import { TestMemoryRouter } from '../routing';
18+
import { useNotificationContext } from '../notification';
19+
import { useTakeUndoableMutation } from './undo';
920

1021
export default { title: 'ra-core/dataProvider/useDelete' };
1122

@@ -162,3 +173,110 @@ const ParamsCore = () => {
162173
</>
163174
);
164175
};
176+
177+
const Notification = () => {
178+
const { notifications, resetNotifications } = useNotificationContext();
179+
const takeMutation = useTakeUndoableMutation();
180+
181+
return notifications.length > 0 ? (
182+
<>
183+
<div>{notifications[0].message}</div>
184+
<div style={{ display: 'flex', gap: '16px' }}>
185+
<button
186+
onClick={() => {
187+
if (notifications[0].notificationOptions?.undoable) {
188+
const mutation = takeMutation();
189+
if (mutation) {
190+
mutation({ isUndo: false });
191+
}
192+
}
193+
resetNotifications();
194+
}}
195+
>
196+
Close
197+
</button>
198+
</div>
199+
</>
200+
) : null;
201+
};
202+
203+
const DeleteButton = ({ mutationMode }: { mutationMode: MutationModeType }) => {
204+
const { isPending, handleDelete } = useDeleteController({
205+
mutationMode,
206+
redirect: 'list',
207+
});
208+
return (
209+
<button onClick={handleDelete} disabled={isPending}>
210+
Delete
211+
</button>
212+
);
213+
};
214+
215+
export const InvalidateList = ({
216+
mutationMode,
217+
}: {
218+
mutationMode: MutationModeType;
219+
}) => {
220+
const dataProvider = fakeRestDataProvider(
221+
{
222+
posts: [
223+
{ id: 1, title: 'Hello' },
224+
{ id: 2, title: 'World' },
225+
],
226+
},
227+
process.env.NODE_ENV !== 'test',
228+
process.env.NODE_ENV === 'test' ? 10 : 1000
229+
);
230+
231+
return (
232+
<TestMemoryRouter initialEntries={['/posts/1']}>
233+
<CoreAdmin dataProvider={dataProvider}>
234+
<Resource
235+
name="posts"
236+
edit={
237+
<EditBase>
238+
<div>
239+
<h1>Edit Post</h1>
240+
<WithRecord
241+
render={record => (
242+
<div>Title: {record.title}</div>
243+
)}
244+
/>
245+
<DeleteButton mutationMode={mutationMode} />
246+
</div>
247+
</EditBase>
248+
}
249+
list={
250+
<ListBase loading={<p>Loading...</p>}>
251+
<RecordsIterator
252+
render={(record: any) => (
253+
<div
254+
style={{
255+
display: 'flex',
256+
gap: '8px',
257+
alignItems: 'center',
258+
}}
259+
>
260+
{record.id}: {record.title}
261+
</div>
262+
)}
263+
/>
264+
<Notification />
265+
</ListBase>
266+
}
267+
/>
268+
</CoreAdmin>
269+
</TestMemoryRouter>
270+
);
271+
};
272+
InvalidateList.args = {
273+
mutationMode: 'undoable',
274+
};
275+
InvalidateList.argTypes = {
276+
mutationMode: {
277+
control: {
278+
type: 'select',
279+
},
280+
options: ['pessimistic', 'optimistic', 'undoable'],
281+
},
282+
};

packages/ra-core/src/dataProvider/useDelete.ts

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -211,34 +211,14 @@ export const useDelete = <
211211

212212
return params.previousData;
213213
},
214-
getSnapshot: ({ resource }) => {
214+
getQueryKeys: ({ resource }) => {
215215
const queryKeys = [
216216
[resource, 'getList'],
217217
[resource, 'getInfiniteList'],
218218
[resource, 'getMany'],
219219
[resource, 'getManyReference'],
220220
];
221-
222-
/**
223-
* Snapshot the previous values via queryClient.getQueriesData()
224-
*
225-
* The snapshotData ref will contain an array of tuples [query key, associated data]
226-
*
227-
* @example
228-
* [
229-
* [['posts', 'getList'], { data: [{ id: 1, title: 'Hello' }], total: 1 }],
230-
* [['posts', 'getMany'], [{ id: 1, title: 'Hello' }]],
231-
* ]
232-
*
233-
* @see https://tanstack.com/query/v5/docs/react/reference/QueryClient#queryclientgetqueriesdata
234-
*/
235-
const snapshot = queryKeys.reduce(
236-
(prev, queryKey) =>
237-
prev.concat(queryClient.getQueriesData({ queryKey })),
238-
[] as Snapshot
239-
);
240-
241-
return snapshot;
221+
return queryKeys;
242222
},
243223
onSettled: (
244224
result,

packages/ra-core/src/dataProvider/useDeleteMany.spec.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { QueryClient, useMutationState } from '@tanstack/react-query';
66
import { CoreAdminContext } from '../core';
77
import { testDataProvider } from './testDataProvider';
88
import { useDeleteMany } from './useDeleteMany';
9-
import { MutationMode, Params } from './useDeleteMany.stories';
9+
import { MutationMode, Params, InvalidateList } from './useDeleteMany.stories';
1010

1111
describe('useDeleteMany', () => {
1212
it('returns a callback that can be used with update arguments', async () => {
@@ -390,5 +390,15 @@ describe('useDeleteMany', () => {
390390
});
391391
});
392392
});
393+
394+
it('invalidates getList query when dataProvider resolves in undoable mode', async () => {
395+
render(<InvalidateList mutationMode="undoable" />);
396+
fireEvent.click(await screen.findByText('Delete'));
397+
await screen.findByText('resources.posts.notifications.deleted');
398+
fireEvent.click(screen.getByText('Close'));
399+
await waitFor(() => {
400+
expect(screen.queryByText('1: Hello')).toBeNull();
401+
});
402+
});
393403
});
394404
});

0 commit comments

Comments
 (0)