Cancel current outgoing mutation when sending new one in create-t3-app?
I'm working on a project which uses optimistic UI updates when a user initiates a trpc mutation (so, e.g. as soon as they click a button to edit a piece of data, the corresponding data in the cache gets updated synchronously, and then overwritten by fresh server data once the request completes).
Have tried to follow the patterns here, and it's been working pretty well: https://tanstack.com/query/v4/docs/guides/optimistic-updates
The only issue I have with this approach is that, if the user is initiating multiple mutations at once this can lead to race conditions (e.g. there's one button which sets a value in a database to equal
1 and there's a different button which, set's that same value to be 2. If the user presses both buttons in quick succession, then whichever of the two requests resolves last will become the final result.
It would be a way better experience if I could make it so that whichever request the user initiated last was the one which ended up as the final result 100% of the time - so I'd love to be able to cancel an old outgoing mutation when the user initiates a new one. Has anyone here developed a pattern to do this before? Would really appreciate taking a look at your solutions 🙏 ❤️Optimistic Updates | TanStack Query Docs
When you optimistically update your state before performing a mutation, there is a chance that the mutation will fail. In most of these failure cases, you can just trigger a refetch for your optimistic queries to revert them to their true server state. In some circumstances though, refetching may not work correctly and the mutation error could ...
14 Replies
mutate return you a isLoading (when its mutating)
you can disable the button to avoid multiple requests
you can "proxy" the event handler
Thanks Neto - I would love to not disable the button though, and instead have the app behave as if these buttons are synchronous from the user's perspective
you can "proxy" the event handlerSorry for interrupting you! Please continue!
if you dont want to have a race condition
you have to throttle the user actions
or you can make a optimistic update and clear the cache after a successful request
I'm already doing the optimstic update and clearing the cache after successful request
But I think even if you clear the cache, that doesn't prevent old request from returning and overwriting the cache again later, and it doesn't guarantee the correct value is set in the database
One way I could do this in theory (but would be really gross), is to have the client send a timestamp with every request, and only execute thing on the server if the server has never seen a timestamp that recent before
but there's gotta be a better way than that 😅
its annoying for the user
but for simplicity sake
blocking multiple requests at the same time its better
is a request is happening, "block" new requests until the prev one is done
So you reckon there's no nice way to tell react query/trpc "Hey forget about that old request we sent before, when it returns please ignore it"?
you can maybe do something with abort control
to avoid racing without blocking actions
but i dont think its a query/trpc issue tho
what you described its a real issue on every type of request/response process
Okay you're right that the best option is to do optimistic UI update on first button press, and then disable the other buttons until that request settles 🙂
Thanks for your help!
glad to help
I think there actually would be a way to build this such that the user felt like they could update a backend-persisted value synchronously, but it would require doing something really clever, where if an old mutation hasn't settled yet we never fire a second one and instead only track changes to the data on the client side, and then update the queryCache and batch all the new updates into a single new mutation the moment old one settles (without ever having there be two outgoing mutations at once)
But like you said, it'd be a bit tricky and easy to mess it up, so going to follow the pattern of disabling any potentially conflicting inputs for now ^_^
If I can't resist and I end up spending the time to build an abstraction for easily doing this later on I'll share here 😛
maybe a "MutationStore"
where you send mutations to be executed in sequence, and execute them in order
Clever is normally wrong
Why not just have the button update the old version on the backend
Then when it returns it receives the version
And can only update to a newer version
Ie click 3 = version 3
There's a chance that click 3 could actually initiate a request before click 2 tho right? Unsure what you do in that scenario
Yeah that totally works Scot, but I think in general, from the user's perspective, whenever we can avoid making them wait for loading spinners/disabling inputs, that's usually a way better experience! (Imagine how awful an app like Canva or Google Docs would be if the whole page froze while things were syncing with the server!)
Yup and those systems are extremely complex
Even "last write wins" algorithms can have issues because of clock drift