-
-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Add support for offline/local first applications #10545
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
Changes from 48 commits
aaf7860
2aa2cba
a1b2157
9d66e05
fe15863
1abc446
25ff38b
9f83cab
eae5aa6
87e537a
b0bc9a0
cf79b3b
78262e9
6287a1c
7d3d2e2
fd94c6a
b87aed7
39ce253
2737f75
07121fd
233ce57
f88c509
8f3096b
9f418bf
6778fd1
934fff4
32af617
02a957a
4c71375
47cdf92
466aeb1
72ff0b1
6d3daeb
c7b22d6
1be8e73
01a1a94
10ae938
9849a2c
4a3cb09
84207e0
2e2e10c
8452c4f
a55d5ef
8384ca3
02dbe24
0ee8837
b648d08
929d606
1154f17
5b7f83a
d114f96
30a5ec0
68d00a7
ed0f0df
d0586a1
b03dfb4
dbd8dd1
b8b13c5
a3b1da8
4413807
3fda18b
66416c1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -888,3 +888,133 @@ export default App; | |||||||||||||||||
``` | ||||||||||||||||||
|
||||||||||||||||||
**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. | ||||||||||||||||||
|
||||||||||||||||||
## Offline Support | ||||||||||||||||||
|
||||||||||||||||||
React-admin supports offline/local-first applications. To enable this feature, install the following react-query packages: | ||||||||||||||||||
|
||||||||||||||||||
```sh | ||||||||||||||||||
yarn add @tanstack/react-query-persist-client @tanstack/query-sync-storage-persister | ||||||||||||||||||
``` | ||||||||||||||||||
|
||||||||||||||||||
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: | ||||||||||||||||||
|
||||||||||||||||||
```ts | ||||||||||||||||||
// in src/queryClient.ts | ||||||||||||||||||
import { addOfflineSupportToQueryClient } from 'react-admin'; | ||||||||||||||||||
import { QueryClient } from '@tanstack/react-query'; | ||||||||||||||||||
import { dataProvider } from './dataProvider'; | ||||||||||||||||||
|
||||||||||||||||||
export const queryClient = new QueryClient(); | ||||||||||||||||||
|
||||||||||||||||||
const queryClientWithOfflineSupport = addOfflineSupportToQueryClient({ | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the point of EDIT: in following examples, it's much clearer.
Suggested change
|
||||||||||||||||||
queryClient, | ||||||||||||||||||
dataProvider, | ||||||||||||||||||
resources: ['posts', 'comments'], | ||||||||||||||||||
}); | ||||||||||||||||||
``` | ||||||||||||||||||
|
||||||||||||||||||
Then, wrap your `<Admin>` inside a [`<PersistQueryClientProvider>`](https://tanstack.com/query/latest/docs/framework/react/plugins/persistQueryClient#persistqueryclientprovider): | ||||||||||||||||||
|
||||||||||||||||||
{% raw %} | ||||||||||||||||||
```tsx | ||||||||||||||||||
// in src/App.tsx | ||||||||||||||||||
import { Admin, Resource } from 'react-admin'; | ||||||||||||||||||
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'; | ||||||||||||||||||
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'; | ||||||||||||||||||
import { queryClient } from './queryClient'; | ||||||||||||||||||
import { dataProvider } from './dataProvider'; | ||||||||||||||||||
import { posts } from './posts'; | ||||||||||||||||||
import { comments } from './comments'; | ||||||||||||||||||
|
||||||||||||||||||
const localStoragePersister = createSyncStoragePersister({ | ||||||||||||||||||
storage: window.localStorage, | ||||||||||||||||||
}); | ||||||||||||||||||
|
||||||||||||||||||
export const App = () => ( | ||||||||||||||||||
<PersistQueryClientProvider | ||||||||||||||||||
client={queryClient} | ||||||||||||||||||
persistOptions={{ persister: localStoragePersister }} | ||||||||||||||||||
onSuccess={() => { | ||||||||||||||||||
// resume mutations after initial restore from localStorage is successful | ||||||||||||||||||
queryClient.resumePausedMutations(); | ||||||||||||||||||
}} | ||||||||||||||||||
> | ||||||||||||||||||
<Admin queryClient={queryClient} dataProvider={dataProvider}> | ||||||||||||||||||
<Resource name="posts" {...posts} /> | ||||||||||||||||||
<Resource name="comments" {...comments} /> | ||||||||||||||||||
</Admin> | ||||||||||||||||||
</PersistQueryClientProvider> | ||||||||||||||||||
) | ||||||||||||||||||
``` | ||||||||||||||||||
{% endraw %} | ||||||||||||||||||
|
||||||||||||||||||
slax57 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||
This is enough to make all the standard react-admin features support offline scenarios. | ||||||||||||||||||
|
||||||||||||||||||
## Adding Offline Support To Custom Mutations | ||||||||||||||||||
|
||||||||||||||||||
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: | ||||||||||||||||||
|
||||||||||||||||||
```ts | ||||||||||||||||||
const dataProvider = { | ||||||||||||||||||
getList: /* ... */, | ||||||||||||||||||
getOne: /* ... */, | ||||||||||||||||||
getMany: /* ... */, | ||||||||||||||||||
getManyReference: /* ... */, | ||||||||||||||||||
create: /* ... */, | ||||||||||||||||||
update: /* ... */, | ||||||||||||||||||
updateMany: /* ... */, | ||||||||||||||||||
delete: /* ... */, | ||||||||||||||||||
deleteMany: /* ... */, | ||||||||||||||||||
banUser: (userId: string) => { | ||||||||||||||||||
return fetch(`/api/user/${userId}/ban`, { method: 'POST' }) | ||||||||||||||||||
.then(response => response.json()); | ||||||||||||||||||
}, | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
export type MyDataProvider = DataProvider & { | ||||||||||||||||||
banUser: (userId: string) => Promise<{ data: RaRecord }> | ||||||||||||||||||
} | ||||||||||||||||||
``` | ||||||||||||||||||
|
||||||||||||||||||
First, you must set a `mutationKey` for this mutation: | ||||||||||||||||||
|
||||||||||||||||||
{% raw %} | ||||||||||||||||||
```tsx | ||||||||||||||||||
const BanUserButton = ({ userId }: { userId: string }) => { | ||||||||||||||||||
const dataProvider = useDataProvider(); | ||||||||||||||||||
const { mutate, isPending } = useMutation({ | ||||||||||||||||||
mutationKey: 'banUser' | ||||||||||||||||||
mutationFn: (userId) => dataProvider.banUser(userId) | ||||||||||||||||||
}); | ||||||||||||||||||
return <Button label="Ban" onClick={() => mutate(userId)} disabled={isPending} />; | ||||||||||||||||||
}; | ||||||||||||||||||
``` | ||||||||||||||||||
{% endraw %} | ||||||||||||||||||
|
||||||||||||||||||
**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. | ||||||||||||||||||
|
||||||||||||||||||
Then, register a default function for it: | ||||||||||||||||||
|
||||||||||||||||||
```ts | ||||||||||||||||||
// in src/queryClient.ts | ||||||||||||||||||
import { addOfflineSupportToQueryClient } from 'react-admin'; | ||||||||||||||||||
import { QueryClient } from '@tanstack/react-query'; | ||||||||||||||||||
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'; | ||||||||||||||||||
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'; | ||||||||||||||||||
erwanMarmelab marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||
import { dataProvider } from './dataProvider'; | ||||||||||||||||||
|
||||||||||||||||||
const baseQueryClient = new QueryClient(); | ||||||||||||||||||
|
||||||||||||||||||
export const queryClient = addOfflineSupportToQueryClient({ | ||||||||||||||||||
queryClient, | ||||||||||||||||||
dataProvider, | ||||||||||||||||||
resources: ['posts', 'comments'], | ||||||||||||||||||
}); | ||||||||||||||||||
|
||||||||||||||||||
queryClient.setMutationDefaults('banUser', { | ||||||||||||||||||
mutationFn: async (userId) => { | ||||||||||||||||||
return dataProviderFn.banUser(userId); | ||||||||||||||||||
slax57 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||
}, | ||||||||||||||||||
}); | ||||||||||||||||||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because react-query now persist queries and mutations for offline mode, the previous test now leaks into the second (e.g. this post has its title changed to Lorem Ipsum). I tried to configure testIsolation in Cypress but our version is probably too old