Skip to content

Commit fe15863

Browse files
committed
Introduce addOfflineSupportToQueryClient
1 parent 9d66e05 commit fe15863

File tree

7 files changed

+173
-77
lines changed

7 files changed

+173
-77
lines changed

examples/simple/src/getOfflineFirstQueryClient.ts

Lines changed: 0 additions & 39 deletions
This file was deleted.

examples/simple/src/index.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
/* eslint react/jsx-key: off */
22
import * as React from 'react';
3-
import { Admin, Resource, CustomRoutes } from 'react-admin'; // eslint-disable-line import/no-unresolved
3+
import {
4+
addOfflineSupportToQueryClient,
5+
Admin,
6+
Resource,
7+
CustomRoutes,
8+
} from 'react-admin';
49
import { createRoot } from 'react-dom/client';
510
import { Route } from 'react-router-dom';
611
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
@@ -16,13 +21,12 @@ import posts from './posts';
1621
import users from './users';
1722
import tags from './tags';
1823
import { queryClient } from './queryClient';
19-
import { getOfflineFirstQueryClient } from './getOfflineFirstQueryClient';
2024

2125
const localStoragePersister = createSyncStoragePersister({
2226
storage: window.localStorage,
2327
});
2428

25-
const offlineFirstQueryClient = getOfflineFirstQueryClient({
29+
addOfflineSupportToQueryClient({
2630
queryClient,
2731
dataProvider,
2832
resources: ['posts', 'comments', 'tags', 'users'],
@@ -34,18 +38,18 @@ const root = createRoot(container);
3438
root.render(
3539
<React.StrictMode>
3640
<PersistQueryClientProvider
37-
client={offlineFirstQueryClient}
41+
client={queryClient}
3842
persistOptions={{ persister: localStoragePersister }}
3943
onSuccess={() => {
40-
// resume mutations after initial restore from localStorage was successful
44+
// resume mutations after initial restore from localStorage is successful
4145
queryClient.resumePausedMutations();
4246
}}
4347
>
4448
<Admin
4549
authProvider={authProvider}
4650
dataProvider={dataProvider}
4751
i18nProvider={i18nProvider}
48-
queryClient={offlineFirstQueryClient}
52+
queryClient={queryClient}
4953
title="Example Admin"
5054
layout={Layout}
5155
>

examples/simple/src/posts/PostCreate.tsx

Lines changed: 47 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
useCreate,
2727
useCreateSuggestionContext,
2828
CanAccess,
29+
MutationMode,
2930
} from 'react-admin';
3031
import { useFormContext, useWatch } from 'react-hook-form';
3132
import {
@@ -35,35 +36,25 @@ import {
3536
DialogActions,
3637
DialogContent,
3738
} from '@mui/material';
38-
39-
/**
40-
* To ensure no existing tests are broken, we keep the same mutation mode as before by default.
41-
* In order to test the new optimistic or undoable modes, you can add `mutationMode=optimistic` or `mutationMode=undoable`
42-
* to the query string.
43-
*/
44-
const getMutationMode = () => {
45-
const querystring = new URLSearchParams(window.location.search);
46-
const mutationMode = querystring.get('mutationMode');
47-
switch (mutationMode) {
48-
case 'undoable':
49-
return 'undoable';
50-
case 'optimistic':
51-
return 'optimistic';
52-
default:
53-
return 'pessimistic';
54-
}
55-
};
39+
import { Link, useSearchParams } from 'react-router-dom';
5640

5741
// Client side id generation. We start from 100 to avoid querying the post list to get the next id as we
5842
// may be offline and accessing this page directly (without going through the list page first) which would
5943
// be possible if the app was also a PWA.
6044
// We only do that for optimistic and undoable modes in order to not break any existing tests that expect
6145
// the id to be generated by the server (e.g. by FakeRest).
6246
let next_id = 100;
63-
const getNewId = () =>
64-
getMutationMode() === 'pessimistic' ? undefined : next_id++;
47+
const getNewId = (mutationMode: MutationMode) => {
48+
const id = mutationMode === 'pessimistic' ? undefined : next_id++;
49+
console.log({ mutationMode, id });
50+
return id;
51+
};
6552

66-
const PostCreateToolbar = () => {
53+
const PostCreateToolbar = ({
54+
mutationMode,
55+
}: {
56+
mutationMode: MutationMode;
57+
}) => {
6758
const notify = useNotify();
6859
const redirect = useRedirect();
6960
const { reset } = useFormContext();
@@ -80,11 +71,16 @@ const PostCreateToolbar = () => {
8071
notify('resources.posts.notifications.created', {
8172
type: 'info',
8273
messageArgs: { smart_count: 1 },
83-
undoable: getMutationMode() === 'undoable',
74+
undoable: mutationMode === 'undoable',
8475
});
8576
redirect('show', 'posts', data.id);
8677
},
8778
}}
79+
transform={data => ({
80+
...data,
81+
id: getNewId(mutationMode),
82+
average_note: 10,
83+
})}
8884
sx={{ display: { xs: 'none', sm: 'flex' } }}
8985
/>
9086
<SaveButton
@@ -98,10 +94,15 @@ const PostCreateToolbar = () => {
9894
notify('resources.posts.notifications.created', {
9995
type: 'info',
10096
messageArgs: { smart_count: 1 },
101-
undoable: getMutationMode() === 'undoable',
97+
undoable: mutationMode === 'undoable',
10298
});
10399
},
104100
}}
101+
transform={data => ({
102+
...data,
103+
id: getNewId(mutationMode),
104+
average_note: 10,
105+
})}
105106
/>
106107
<SaveButton
107108
label="post.action.save_with_average_note"
@@ -112,14 +113,14 @@ const PostCreateToolbar = () => {
112113
notify('resources.posts.notifications.created', {
113114
type: 'info',
114115
messageArgs: { smart_count: 1 },
115-
undoable: getMutationMode() === 'undoable',
116+
undoable: mutationMode === 'undoable',
116117
});
117118
redirect('show', 'posts', data.id);
118119
},
119120
}}
120121
transform={data => ({
121122
...data,
122-
id: getNewId(),
123+
id: getNewId(mutationMode),
123124
average_note: 10,
124125
})}
125126
sx={{ display: { xs: 'none', sm: 'flex' } }}
@@ -135,28 +136,43 @@ const backlinksDefaultValue = [
135136
},
136137
];
137138

139+
const useMutationMode = (): MutationMode => {
140+
const [searchParams] = useSearchParams();
141+
const mutationMode = searchParams.get('mutationMode') ?? 'pessimistic';
142+
143+
return ['optimistic', 'undoable', 'pessimistic'].includes(mutationMode)
144+
? (mutationMode as MutationMode)
145+
: 'pessimistic';
146+
};
147+
138148
const PostCreate = () => {
139149
const defaultValues = useMemo(
140150
() => ({
141151
average_note: 0,
142152
}),
143153
[]
144154
);
155+
const mutationMode = useMutationMode();
145156
const dateDefaultValue = useMemo(() => new Date(), []);
146157
return (
147158
<Create
148159
redirect="edit"
149-
mutationMode={getMutationMode()}
150-
transform={data => ({ ...data, id: getNewId() })}
160+
mutationMode={mutationMode}
161+
transform={data => ({ ...data, id: getNewId(mutationMode) })}
151162
>
152163
<Alert severity="info">
153164
To test offline support, add either{' '}
154-
<code>?mutationMode=optimistic</code> or{' '}
155-
<code>?mutationMode=undoable</code> to the page search
156-
parameters.
165+
<Link to="/posts/create?mutationMode=optimistic">
166+
<code>?mutationMode=optimistic</code>
167+
</Link>{' '}
168+
or
169+
<Link to="/posts/create?mutationMode=undoable">
170+
<code>?mutationMode=undoable</code>
171+
</Link>{' '}
172+
to the page search parameters.
157173
</Alert>
158174
<SimpleFormConfigurable
159-
toolbar={<PostCreateToolbar />}
175+
toolbar={<PostCreateToolbar mutationMode={mutationMode} />}
160176
defaultValues={defaultValues}
161177
sx={{ maxWidth: { md: 'auto', lg: '30em' } }}
162178
>
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import type { QueryClient } from '@tanstack/react-query';
2+
import { reactAdminMutations } from './dataFetchActions';
3+
import type { DataProvider } from '../types';
4+
5+
/**
6+
* A function that registers default functions on the queryClient for the specified mutations and resources.
7+
* react-query requires default mutation functions to allow resumable mutations
8+
* (e.g. mutations triggered while offline and users navigated away from the component that triggered them).
9+
*
10+
* @example <caption>Adding offline support for the default mutations</caption>
11+
* // in src/App.tsx
12+
* import { Admin, Resource, addOfflineSupportToQueryClient, reactAdminMutations } from 'react-admin';
13+
* import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
14+
* import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
15+
* import { queryClient } from './queryClient';
16+
* import { dataProvider } from './dataProvider';
17+
* import { posts } from './posts';
18+
* import { comments } from './comments';
19+
*
20+
* const localStoragePersister = createSyncStoragePersister({
21+
* storage: window.localStorage,
22+
* });
23+
*
24+
* addOfflineSupportToQueryClient({
25+
* queryClient,
26+
* dataProvider,
27+
* resources: ['posts', 'comments'],
28+
* mutations: [...reactAdminMutations, 'myCustomMutation'],
29+
* });
30+
*
31+
* const App = () => (
32+
* <PersistQueryClientProvider
33+
* client={queryClient}
34+
* persistOptions={{ persister: localStoragePersister }}
35+
* onSuccess={() => {
36+
* // resume mutations after initial restore from localStorage was successful
37+
* queryClient.resumePausedMutations();
38+
* }}
39+
* >
40+
* <Admin queryClient={queryClient} dataProvider={dataProvider}>
41+
* <Resource name="posts" {...posts} />
42+
* <Resource name="comments" {...comments} />
43+
* </Admin>
44+
* </PersistQueryClientProvider>
45+
* );
46+
*
47+
* @example <caption>Adding offline support with custom mutations</caption>
48+
* // in src/App.tsx
49+
* import { addOfflineSupportToQueryClient } from 'react-admin';
50+
* import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
51+
* import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
52+
* import { queryClient } from './queryClient';
53+
* import { dataProvider } from './dataProvider';
54+
* import { posts } from './posts';
55+
* import { comments } from './comments';
56+
*
57+
* const localStoragePersister = createSyncStoragePersister({
58+
* storage: window.localStorage,
59+
* });
60+
*
61+
* addOfflineSupportToQueryClient({
62+
* queryClient,
63+
* dataProvider,
64+
* resources: ['posts', 'comments'],
65+
* });
66+
*
67+
* const App = () => (
68+
* <PersistQueryClientProvider
69+
* client={queryClient}
70+
* persistOptions={{ persister: localStoragePersister }}
71+
* onSuccess={() => {
72+
* // resume mutations after initial restore from localStorage was successful
73+
* queryClient.resumePausedMutations();
74+
* }}
75+
* >
76+
* <Admin queryClient={queryClient} dataProvider={dataProvider}>
77+
* <Resource name="posts" {...posts} />
78+
* <Resource name="comments" {...comments} />
79+
* </Admin>
80+
* </PersistQueryClientProvider>
81+
* );
82+
*/
83+
export const addOfflineSupportToQueryClient = ({
84+
dataProvider,
85+
resources,
86+
queryClient,
87+
}: {
88+
dataProvider: DataProvider;
89+
resources: string[];
90+
queryClient: QueryClient;
91+
}) => {
92+
resources.forEach(resource => {
93+
reactAdminMutations.forEach(mutation => {
94+
queryClient.setMutationDefaults([resource, mutation], {
95+
mutationFn: async params => {
96+
const dataProviderFn = dataProvider[mutation] as Function;
97+
return dataProviderFn.apply(dataProviderFn, ...params);
98+
},
99+
});
100+
});
101+
});
102+
};

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ export const UPDATE_MANY = 'UPDATE_MANY';
88
export const DELETE = 'DELETE';
99
export const DELETE_MANY = 'DELETE_MANY';
1010

11+
export const reactAdminMutations = [
12+
'create',
13+
'delete',
14+
'update',
15+
'updateMany',
16+
'deleteMany',
17+
];
18+
1119
export const fetchActionsWithRecordResponse = ['getOne', 'create', 'update'];
1220
export const fetchActionsWithArrayOfIdentifiedRecordsResponse = [
1321
'getList',

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import HttpError from './HttpError';
44
import * as fetchUtils from './fetch';
55
import undoableEventEmitter from './undoableEventEmitter';
66

7+
export * from './addOfflineSupportToQueryClient';
78
export * from './combineDataProviders';
89
export * from './dataFetchActions';
910
export * from './defaultDataProvider';

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useMemo, useRef } from 'react';
1+
import { useEffect, useMemo, useRef } from 'react';
22
import {
33
useMutation,
44
UseMutationOptions,
@@ -96,7 +96,11 @@ export const useCreate = <
9696
getMutateWithMiddlewares,
9797
...mutationOptions
9898
} = options;
99+
99100
const mode = useRef<MutationMode>(mutationMode);
101+
useEffect(() => {
102+
mode.current = mutationMode;
103+
}, [mutationMode]);
100104

101105
const paramsRef =
102106
useRef<Partial<CreateParams<Partial<RecordType>>>>(params);

0 commit comments

Comments
 (0)