Skip to content

Commit 9159853

Browse files
authored
Merge pull request #11152 from marmelab/offline-demo
[Doc] Offline Support Documentation
2 parents cd64af9 + b7b07ae commit 9159853

File tree

9 files changed

+723
-1
lines changed

9 files changed

+723
-1
lines changed

docs/DataProviders.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -888,3 +888,133 @@ export default App;
888888
```
889889

890890
**Tip**: This example uses the function version of `setState` (`setDataProvider(() => dataProvider)`) instead of the more classic version (`setDataProvider(dataProvider)`). This is because some legacy Data Providers are actually functions, and `setState` would call them immediately on mount.
891+
892+
## Offline Support
893+
894+
React-admin supports offline/local-first applications. To enable this feature, install the following react-query packages:
895+
896+
```sh
897+
yarn add @tanstack/react-query-persist-client @tanstack/query-async-storage-persister
898+
```
899+
900+
Then, register default functions for react-admin mutations on the `QueryClient` to enable resumable mutations (mutations triggered while offline). React-admin provides the `addOfflineSupportToQueryClient` function for this:
901+
902+
```ts
903+
// in src/queryClient.ts
904+
import { addOfflineSupportToQueryClient } from 'react-admin';
905+
import { QueryClient } from '@tanstack/react-query';
906+
import { dataProvider } from './dataProvider';
907+
908+
const baseQueryClient = new QueryClient();
909+
910+
export const queryClient = addOfflineSupportToQueryClient({
911+
queryClient: baseQueryClient,
912+
dataProvider,
913+
resources: ['posts', 'comments'],
914+
});
915+
```
916+
917+
Finally, wrap your `<Admin>` inside a [`<PersistQueryClientProvider>`](https://tanstack.com/query/latest/docs/framework/react/plugins/persistQueryClient#persistqueryclientprovider):
918+
919+
{% raw %}
920+
```tsx
921+
// in src/App.tsx
922+
import { Admin, Resource } from 'react-admin';
923+
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
924+
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
925+
import { queryClient } from './queryClient';
926+
import { dataProvider } from './dataProvider';
927+
import { posts } from './posts';
928+
import { comments } from './comments';
929+
930+
const localStoragePersister = createAsyncStoragePersister({
931+
storage: window.localStorage,
932+
});
933+
934+
export const App = () => (
935+
<PersistQueryClientProvider
936+
client={queryClient}
937+
persistOptions={{ persister: localStoragePersister }}
938+
onSuccess={() => {
939+
// resume mutations after initial restore from localStorage is successful
940+
queryClient.resumePausedMutations();
941+
}}
942+
>
943+
<Admin queryClient={queryClient} dataProvider={dataProvider}>
944+
<Resource name="posts" {...posts} />
945+
<Resource name="comments" {...comments} />
946+
</Admin>
947+
</PersistQueryClientProvider>
948+
)
949+
```
950+
{% endraw %}
951+
952+
This is enough to make all the standard react-admin features support offline scenarios.
953+
954+
## Adding Offline Support To Custom Mutations
955+
956+
If you have [custom mutations](./Actions.md#calling-custom-methods) on your dataProvider, you can enable offline support for them too. For instance, if your `dataProvider` exposes a `banUser()` method:
957+
958+
```ts
959+
const dataProvider = {
960+
getList: /* ... */,
961+
getOne: /* ... */,
962+
getMany: /* ... */,
963+
getManyReference: /* ... */,
964+
create: /* ... */,
965+
update: /* ... */,
966+
updateMany: /* ... */,
967+
delete: /* ... */,
968+
deleteMany: /* ... */,
969+
banUser: (userId: string) => {
970+
return fetch(`/api/user/${userId}/ban`, { method: 'POST' })
971+
.then(response => response.json());
972+
},
973+
}
974+
975+
export type MyDataProvider = DataProvider & {
976+
banUser: (userId: string) => Promise<{ data: RaRecord }>
977+
}
978+
```
979+
980+
First, you must set a `mutationKey` for this mutation:
981+
982+
{% raw %}
983+
```tsx
984+
const BanUserButton = ({ userId }: { userId: string }) => {
985+
const dataProvider = useDataProvider();
986+
const { mutate, isPending } = useMutation({
987+
mutationKey: ['banUser'],
988+
mutationFn: (userId) => dataProvider.banUser(userId)
989+
});
990+
return <Button label="Ban" onClick={() => mutate(userId)} disabled={isPending} />;
991+
};
992+
```
993+
{% endraw %}
994+
995+
**Tip**: Note that unlike the [_Calling Custom Methods_ example](./Actions.md#calling-custom-methods), we passed `userId` to the `mutate` function. This is necessary so that React Query passes it too to the default function when resuming the mutation.
996+
997+
Then, register a default function for it:
998+
999+
```ts
1000+
// in src/queryClient.ts
1001+
import { addOfflineSupportToQueryClient } from 'react-admin';
1002+
import { QueryClient } from '@tanstack/react-query';
1003+
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
1004+
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
1005+
import { dataProvider } from './dataProvider';
1006+
1007+
const baseQueryClient = new QueryClient();
1008+
1009+
export const queryClient = addOfflineSupportToQueryClient({
1010+
queryClient: baseQueryClient,
1011+
dataProvider,
1012+
resources: ['posts', 'comments'],
1013+
});
1014+
1015+
queryClient.setMutationDefaults('banUser', {
1016+
mutationFn: async (userId) => {
1017+
return dataProvider.banUser(userId);
1018+
},
1019+
});
1020+
```

docs_headless/src/content/docs/DataProviders.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,3 +876,133 @@ export default App;
876876
```
877877

878878
**Tip**: This example uses the function version of `setState` (`setDataProvider(() => dataProvider)`) instead of the more classic version (`setDataProvider(dataProvider)`). This is because some legacy Data Providers are actually functions, and `setState` would call them immediately on mount.
879+
880+
## Offline Support
881+
882+
React-admin supports offline/local-first applications. To enable this feature, install the following react-query packages:
883+
884+
```sh
885+
yarn add @tanstack/react-query-persist-client @tanstack/query-async-storage-persister
886+
```
887+
888+
Then, register default functions for react-admin mutations on the `QueryClient` to enable resumable mutations (mutations triggered while offline). React-admin provides the `addOfflineSupportToQueryClient` function for this:
889+
890+
```ts
891+
// in src/queryClient.ts
892+
import { addOfflineSupportToQueryClient } from 'ra-core';
893+
import { QueryClient } from '@tanstack/react-query';
894+
import { dataProvider } from './dataProvider';
895+
896+
const baseQueryClient = new QueryClient();
897+
898+
export const queryClient = addOfflineSupportToQueryClient({
899+
queryClient: baseQueryClient,
900+
dataProvider,
901+
resources: ['posts', 'comments'],
902+
});
903+
```
904+
905+
Finally, wrap your `<Admin>` inside a [`<PersistQueryClientProvider>`](https://tanstack.com/query/latest/docs/framework/react/plugins/persistQueryClient#persistqueryclientprovider):
906+
907+
{% raw %}
908+
```tsx
909+
// in src/App.tsx
910+
import { CoreAdmin, Resource } from 'ra-core';
911+
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
912+
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
913+
import { queryClient } from './queryClient';
914+
import { dataProvider } from './dataProvider';
915+
import { posts } from './posts';
916+
import { comments } from './comments';
917+
918+
const localStoragePersister = createAsyncStoragePersister({
919+
storage: window.localStorage,
920+
});
921+
922+
export const App = () => (
923+
<PersistQueryClientProvider
924+
client={queryClient}
925+
persistOptions={{ persister: localStoragePersister }}
926+
onSuccess={() => {
927+
// resume mutations after initial restore from localStorage is successful
928+
queryClient.resumePausedMutations();
929+
}}
930+
>
931+
<CoreAdmin queryClient={queryClient} dataProvider={dataProvider}>
932+
<Resource name="posts" {...posts} />
933+
<Resource name="comments" {...comments} />
934+
</CoreAdmin>
935+
</PersistQueryClientProvider>
936+
)
937+
```
938+
{% endraw %}
939+
940+
This is enough to make all the standard react-admin features support offline scenarios.
941+
942+
## Adding Offline Support To Custom Mutations
943+
944+
If you have [custom mutations](./Actions.md#calling-custom-methods) on your dataProvider, you can enable offline support for them too. For instance, if your `dataProvider` exposes a `banUser()` method:
945+
946+
```ts
947+
const dataProvider = {
948+
getList: /* ... */,
949+
getOne: /* ... */,
950+
getMany: /* ... */,
951+
getManyReference: /* ... */,
952+
create: /* ... */,
953+
update: /* ... */,
954+
updateMany: /* ... */,
955+
delete: /* ... */,
956+
deleteMany: /* ... */,
957+
banUser: (userId: string) => {
958+
return fetch(`/api/user/${userId}/ban`, { method: 'POST' })
959+
.then(response => response.json());
960+
},
961+
}
962+
963+
export type MyDataProvider = DataProvider & {
964+
banUser: (userId: string) => Promise<{ data: RaRecord }>
965+
}
966+
```
967+
968+
First, you must set a `mutationKey` for this mutation:
969+
970+
{% raw %}
971+
```tsx
972+
const BanUserButton = ({ userId }: { userId: string }) => {
973+
const dataProvider = useDataProvider();
974+
const { mutate, isPending } = useMutation({
975+
mutationKey: ['banUser'],
976+
mutationFn: (userId) => dataProvider.banUser(userId)
977+
});
978+
return <button onClick={() => mutate(userId)} disabled={isPending}>Ban</button>;
979+
};
980+
```
981+
{% endraw %}
982+
983+
**Tip**: Note that unlike the [_Calling Custom Methods_ example](./Actions.md#calling-custom-methods), we passed `userId` to the `mutate` function. This is necessary so that React Query passes it too to the default function when resuming the mutation.
984+
985+
Then, register a default function for it:
986+
987+
```ts
988+
// in src/queryClient.ts
989+
import { addOfflineSupportToQueryClient } from 'ra-core';
990+
import { QueryClient } from '@tanstack/react-query';
991+
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
992+
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
993+
import { dataProvider } from './dataProvider';
994+
995+
const baseQueryClient = new QueryClient();
996+
997+
export const queryClient = addOfflineSupportToQueryClient({
998+
queryClient: baseQueryClient,
999+
dataProvider,
1000+
resources: ['posts', 'comments'],
1001+
});
1002+
1003+
queryClient.setMutationDefaults('banUser', {
1004+
mutationFn: async (userId) => {
1005+
return dataProvider.banUser(userId);
1006+
},
1007+
});
1008+
```

packages/ra-core/src/auth/useCanAccessCallback.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
} from '@tanstack/react-query';
66
import useAuthProvider from './useAuthProvider';
77
import { HintedString } from '../types';
8+
import { useMemo } from 'react';
89

910
/**
1011
* A hook that returns a function you can call to determine whether user has access to the given resource
@@ -49,6 +50,7 @@ export const useCanAccessCallback = <
4950
> = {}
5051
) => {
5152
const authProvider = useAuthProvider();
53+
const authProviderHasCanAccess = !!authProvider?.canAccess;
5254

5355
const { mutateAsync } = useMutation<
5456
UseCanAccessCallbackResult,
@@ -67,7 +69,10 @@ export const useCanAccessCallback = <
6769
...options,
6870
});
6971

70-
return mutateAsync;
72+
return useMemo(
73+
() => (authProviderHasCanAccess ? mutateAsync : () => true),
74+
[authProviderHasCanAccess, mutateAsync]
75+
);
7176
};
7277

7378
export type UseCanAccessCallback<
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import type { QueryClient } from '@tanstack/react-query';
2+
import { DATAPROVIDER_MUTATIONS } 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 (https://tanstack.com/query/latest/docs/framework/react/guides/mutations#persisting-offline-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 { addOfflineSupportToQueryClient } from 'react-admin';
13+
* import { QueryClient } from '@tanstack/react-query';
14+
* import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
15+
* import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
16+
* import { dataProvider } from './dataProvider';
17+
* import { posts } from './posts';
18+
* import { comments } from './comments';
19+
*
20+
* const localStoragePersister = createAsyncStoragePersister({
21+
* storage: window.localStorage,
22+
* });
23+
*
24+
* const queryClient = addOfflineSupportToQueryClient({
25+
* queryClient: new QueryClient(),
26+
* dataProvider,
27+
* resources: ['posts', 'comments'],
28+
* });
29+
*
30+
* const App = () => (
31+
* <PersistQueryClientProvider
32+
* client={queryClient}
33+
* persistOptions={{ persister: localStoragePersister }}
34+
* onSuccess={() => {
35+
* // resume mutations after initial restore from localStorage was successful
36+
* queryClient.resumePausedMutations();
37+
* }}
38+
* >
39+
* <Admin queryClient={queryClient} dataProvider={dataProvider}>
40+
* <Resource name="posts" {...posts} />
41+
* <Resource name="comments" {...comments} />
42+
* </Admin>
43+
* </PersistQueryClientProvider>
44+
* );
45+
*
46+
* @example <caption>Adding offline support with custom mutations</caption>
47+
* // in src/App.tsx
48+
* import { Admin, Resource, addOfflineSupportToQueryClient, DataProviderMutations } from 'react-admin';
49+
* import { QueryClient } from '@tanstack/react-query';
50+
* import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
51+
* import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
52+
* import { dataProvider } from './dataProvider';
53+
* import { posts } from './posts';
54+
* import { comments } from './comments';
55+
*
56+
* const localStoragePersister = createAsyncStoragePersister({
57+
* storage: window.localStorage,
58+
* });
59+
*
60+
* const queryClient = addOfflineSupportToQueryClient({
61+
* queryClient: new QueryClient(),
62+
* dataProvider,
63+
* resources: ['posts', 'comments'],
64+
* });
65+
*
66+
* const App = () => (
67+
* <PersistQueryClientProvider
68+
* client={queryClient}
69+
* persistOptions={{ persister: localStoragePersister }}
70+
* onSuccess={() => {
71+
* // resume mutations after initial restore from localStorage was successful
72+
* queryClient.resumePausedMutations();
73+
* }}
74+
* >
75+
* <Admin queryClient={queryClient} dataProvider={dataProvider}>
76+
* <Resource name="posts" {...posts} />
77+
* <Resource name="comments" {...comments} />
78+
* </Admin>
79+
* </PersistQueryClientProvider>
80+
* );
81+
*/
82+
export const addOfflineSupportToQueryClient = ({
83+
dataProvider,
84+
resources,
85+
queryClient,
86+
}: {
87+
dataProvider: DataProvider;
88+
resources: string[];
89+
queryClient: QueryClient;
90+
}) => {
91+
resources.forEach(resource => {
92+
DATAPROVIDER_MUTATIONS.forEach(mutation => {
93+
queryClient.setMutationDefaults([resource, mutation], {
94+
mutationFn: async (params: any) => {
95+
const dataProviderFn = dataProvider[mutation] as Function;
96+
return dataProviderFn.apply(dataProviderFn, ...params);
97+
},
98+
});
99+
});
100+
});
101+
102+
return queryClient;
103+
};

0 commit comments

Comments
 (0)