-
-
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
Open
djhi
wants to merge
59
commits into
next
Choose a base branch
from
support-offline-mode
base: next
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
59 commits
Select commit
Hold shift + click to select a range
aaf7860
Add support for offline/local first applications
djhi 2aa2cba
Ensure no existing tests are broken
djhi a1b2157
Try stabilizing tests
djhi 9d66e05
Fix e2e tests
djhi fe15863
Introduce `addOfflineSupportToQueryClient`
djhi 1abc446
Add documentation
djhi 25ff38b
Trigger build
djhi 9f83cab
Merge remote-tracking branch 'origin/next' into support-offline-mode
slax57 eae5aa6
fix merge issues
slax57 87e537a
Merge branch 'next' into support-offline-mode
djhi b0bc9a0
Cleanup documentation
djhi cf79b3b
Fix e2e tests
djhi 78262e9
Merge branch 'next' into support-offline-mode
djhi 6287a1c
Fix documentation
djhi 7d3d2e2
Remove unnecessary ts-ignore
djhi fd94c6a
Apply suggestions from code review
djhi b87aed7
Apply review suggestions
djhi 39ce253
Update useListContextWithProps
djhi 2737f75
Improve post creation ux
djhi 07121fd
Fix e2e tests
djhi 233ce57
Add offline message to ReferenceField
djhi f88c509
Add offline support to list components
djhi 8f3096b
Add offline support to reference components
djhi 9f418bf
Add offline support to details components
djhi 6778fd1
Fix tests and stories
djhi 934fff4
Use Offline everywhere
djhi 32af617
Add support for offline in Reference inputs
djhi 02a957a
Add documentation
djhi 4c71375
Fix documentation
djhi 47cdf92
Improve EditView types
djhi 466aeb1
Apply suggestions from code review
djhi 72ff0b1
Fix exports
djhi 6d3daeb
Fix list children handling of offline state
djhi c7b22d6
Fix detail views handling of offline state
djhi 1be8e73
Fix reference inputs handling of offline state
djhi 01a1a94
Fix ReferenceOneField
djhi 10ae938
Improve ReferenceInput
djhi 9849a2c
Correctly export Offline component
djhi 4a3cb09
Avoid offline specific styles in details views
djhi 84207e0
Rename isOnline hook to useIsOffline
djhi 2e2e10c
Improve Offline component design for reference fields and inputs
djhi 8452c4f
Make sure users know about pending operations
djhi a55d5ef
Improve mutation mode selector
djhi 8384ca3
Improve documentation
djhi 02dbe24
Improve notifications for offline mutations
djhi 0ee8837
Merge branch 'next' into support-offline-mode
djhi b648d08
Fix delete mutations success message handling
djhi 929d606
Fix ReferenceOneField empty case handling
djhi 1154f17
Fix simple example dependencies
djhi 5b7f83a
Fix JSDoc examples
djhi d114f96
Remove unnecessary CSS classes
djhi 30a5ec0
Fix Offline types
djhi 68d00a7
Fix LoadingIndicator
djhi ed0f0df
Fix SingleFieldList story
djhi d0586a1
Fix ReferenceInput offline detection
djhi b03dfb4
Fix reference fields and inputs
djhi dbd8dd1
Improve documentation
djhi b8b13c5
Fix yarn.lock
djhi a3b1da8
Document how to handle errors for resumed mutations
djhi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -888,3 +888,170 @@ 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'; | ||
|
||
const baseQueryClient = new QueryClient(); | ||
|
||
export const queryClient = addOfflineSupportToQueryClient({ | ||
queryClient: baseQueryClient, | ||
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. 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. Added |
||
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: baseQueryClient, | ||
dataProvider, | ||
resources: ['posts', 'comments'], | ||
}); | ||
|
||
queryClient.setMutationDefaults('banUser', { | ||
mutationFn: async (userId) => { | ||
return dataProvider.banUser(userId); | ||
}, | ||
}); | ||
``` | ||
|
||
## Handling Errors For Resumed Mutations | ||
|
||
If you enabled offline support, users might trigger mutations while being actually offline. When they're back online, react-query will _resume_ those mutations and they might fail for other reasons (server side validation or errors). However, as users might have navigated away from the page that triggered the mutation, they won't see any notification. | ||
|
||
To handle this scenario, you must register default `onError` side effects for all mutations (react-admin default ones or custom). If you want to leverage react-admin notifications, you can use a custom layout: | ||
|
||
```tsx | ||
// in src/Layout.tsx | ||
export const MyLayout = ({ children }: { children: React.ReactNode }) => { | ||
const queryClient = useQueryClient(); | ||
const notify = useNotify(); | ||
|
||
React.useEffect(() => { | ||
const mutationKeyFilter = []; // An empty array targets all mutations | ||
queryClient.setMutationDefaults([], { | ||
onSettled(data, error) { | ||
if (error) { | ||
notify(error.message, { type: 'error' }); | ||
} | ||
}, | ||
}); | ||
}, [queryClient, notify]); | ||
|
||
return ( | ||
<Layout> | ||
{children} | ||
</Layout> | ||
); | ||
} | ||
``` | ||
|
||
Note that this simple example will only show the error message as it was received. Users may not have the context to understand the error (what record or operation it relates to). | ||
Here are some ideas for a better user experience: | ||
|
||
- make sure your messages allow users to go to the pages related to the errors (you can leverage [custom notifications](./useNotify.md#custom-notification-content) for that) | ||
- store the notifications somewhere (server side or not) and show them in a custom page with proper links, etc. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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