Offline persistent mechanism for the error state
I am using TanStack in a React Native environment and trying to provide a smooth offline/low connection experience for users.
I use PersistQueryClientProvider with AsyncStorage, and offline mode generally works well. However, I expected mutations to persist during error retries, even if a mutation fails mid-process. This is important for users with low connectivity—we don’t want to lose their data if a mutation fails and the user closes the app.
The use case that I am trying to solve is:
If a user with a poor internet connection triggers a mutation, the request might take too long and eventually throw a timeout error. The system will retry the mutation several times based on the query client configuration. However, if the user closes the app during these retries, the mutation is lost—whereas the expectation is that the mutation operation would resume once the app is reopened.
Is it possible to persist mutations after errors, similar to how queries are persisted?
10 Replies
exotic-emeraldOP•2mo ago
(I would like to avoid copying the logic into a custom layer that would work with Async Storage to keep retrying the promise until it completes)
afraid-scarlet•2mo ago
you can customize what you persist
we default to only successful queries because errors aren't serializable per default, and pending queries have promises that also don't serialize
but if you know how to handle these - you can customize the
PersistQueryClientProvider
set persistOptions: { dehydrateOptions: { shouldDehydrateMutation: (mutation) => ... and return true for those you want to persistexotic-emeraldOP•2mo ago
yes, its the same way I am doing for the offline, the only difference is that I want to keep retrying for some specific errors (timeout, 500, etc)
For my understanding, if I do something like this, would be enough:
dehydrateOptions: {
shouldDehydrateMutation: (mutation) => {
return mutation.state.isPaused || Boolean(mutation.state.error);
},
},
My question is, when calling the queryClient.resumePausedMutations(), will it consider the errors as well?
From the tests, it looks like the mutation with the error state is now persisted. The problem is that the resumePaused when it is called; it only executes the persisted mutations once, and not until it succeeds. Am I missing something?
afraid-scarlet•2mo ago
during retries, you shouldn't have errors yet
resumePausedMutations only resumes paused mutations. If the mutation isn't paused, it won't be resumed
The error can't persist unless you have a custom serializer. Error instances are not JSON serializable
exotic-emeraldOP•2mo ago
exotic-emeraldOP•2mo ago
It's now working using superjson, but I found an "unexpected" behaviour. Let me try to explain. In our use case, we have dependent mutations, let's say, the main mutation depends on two dependent mutation calls internally:
- main
- promise1
- promise2
Reference: https://codefile.io/f/QR0sPee16x
During the mutation, I set the network as offline when "DEBUG:promise1 rejected" and then refreshed the device. When I open the application again, only the "promise1" resumes.
I was expecting to see the "promise2" and/or "test" being executed.
Am I missing something?
Codefile.io
Untitled file — Codefile
Create collaborative code files online for your technical interviews, pair programming, teaching, etc.
afraid-scarlet•2mo ago
mutation 1 is probably the only paused mutation that gets saved. mutation2 has likely never started and the main mutation was just in pending state (not paused)
I would just combine these into one mutation that fires 3 requests after each other.
exotic-emeraldOP•2mo ago
My current approach in my application is to handle everything within a single mutation. My concern is that if something fails during the process, such as a connection issue, the entire operation will need to be restarted from the beginning. I don't see an alternative except to implement all the error handling manually.
Our dependent requests (use case):
1. Call endpoint to upload multiple images to the CDN (N requests)
2. Call endpoint to create a task (1 request)
3. Call endpoint to attach images to the task (1 request)
In this case, using a single mutation to handle this, if something happen in point 3, everything run again from the beginning.
afraid-scarlet•2mo ago
yeah if you just use one mutation, you shouldn't have the problem.
exotic-emeraldOP•2mo ago
(Updated my last message)
My concern is that connection issues occur frequently with our product, and this approach could result in a lot of unnecessary data accumulating in the database or CDN. However, it's a positive that data is at least persisted and can be retried.
@TkDodo 🔮 is there any issue/concern on calling queryClient.resumeMutations from the background-fetch service (expo plugin) ?