-
-
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
base: next
Are you sure you want to change the base?
Changes from all 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
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 |
---|---|---|
|
@@ -884,3 +884,129 @@ 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({ | ||
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 %} | ||
|
||
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. You're missing a sentence to explain that the setup is done here - the rest is only for developers with custom methods. |
||
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); | ||
}, | ||
}); | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -53,12 +53,13 @@ Each `<DataTable.Col>` defines one column of the table: its `source` (used for s | |
| `empty` | Optional | Element | `<Empty>` | The component to render when the list is empty. | | ||
| `expand` | Optional | Element | | The component rendering the expand panel for each row. | | ||
| `expandSingle` | Optional | Boolean | `false` | Whether to allow only one expanded row at a time. | | ||
| `head` | Optional | Element | `<DataTable Header>` | The component rendering the table header. | | ||
| `head` | Optional | Element | `<DataTable Header>` | The component rendering the table header. | | ||
| `hiddenColumns` | Optional | Array | `[]` | The list of columns to hide by default. | | ||
| `foot` | Optional | Element | | The component rendering the table footer. | | ||
| `hover` | Optional | Boolean | `true` | Whether to highlight the row under the mouse. | | ||
| `isRowExpandable` | Optional | Function | `() => true` | A function that returns whether a row is expandable. | | ||
| `isRowSelectable` | Optional | Function | `() => true` | A function that returns whether a row is selectable. | | ||
| `offline` | Optional | Element | `<Offline>` | The content rendered to render when data could not be fetched because of connectivity issues. | | ||
| `rowClick` | Optional | mixed | | The action to trigger when the user clicks on a row. | | ||
| `rowSx` | Optional | Function | | A function that returns the `sx` prop to apply to a row. | | ||
| `size` | Optional | `'small'` or `'medium'` | `'small'` | The size of the table. | | ||
|
@@ -754,6 +755,38 @@ export const PostList = () => ( | |
``` | ||
{% endraw %} | ||
|
||
## `offline` | ||
|
||
It's possible that a `<DataTable>` will have no records to display because of connectivity issues. In that case, `<DataTable>` will display a message indicating data couldn't be fetched. | ||
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. I think you should include the default English translation for the offline message in the doc, to make it searchable. |
||
|
||
You can customize this message via react-admin's [translation system](./Translation.md), by setting a custom translation for the `ra.notification.offline` key. | ||
|
||
```tsx | ||
const messages = { | ||
ra: { | ||
notification: { | ||
offline: "No network. Data couldn't be fetched.", | ||
} | ||
} | ||
} | ||
``` | ||
|
||
If you need to go beyond text, pass a custom element as the `<DataTable offline>` prop: | ||
|
||
```tsx | ||
const Offline = () => ( | ||
<p>No network. Data couldn't be fetched.</p> | ||
); | ||
|
||
const BookList = () => ( | ||
<List> | ||
<DataTable offline={<Offline />}> | ||
... | ||
</DataTable> | ||
</List> | ||
); | ||
``` | ||
|
||
## `rowClick` | ||
|
||
By default, `<DataTable>` uses the current [resource definition](https://marmelab.com/react-admin/Resource.html) to determine what to do when the user clicks on a row. If the resource has a `show` page, a row click redirects to the Show view. If the resource has an `edit` page, a row click redirects to the Edit view. Otherwise, the row is not clickable. | ||
|
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