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
Neto
Neto4y ago
mutate return you a isLoading (when its mutating) you can disable the button to avoid multiple requests you can "proxy" the event handler
X4
X4OP4y ago
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 handler
Sorry for interrupting you! Please continue!
Neto
Neto4y ago
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
X4
X4OP4y ago
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 😅
Neto
Neto4y ago
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
X4
X4OP4y ago
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"?
Neto
Neto4y ago
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
X4
X4OP4y ago
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!
Neto
Neto4y ago
glad to help
X4
X4OP4y ago
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 😛
Neto
Neto4y ago
maybe a "MutationStore" where you send mutations to be executed in sequence, and execute them in order
Scot
Scot4y ago
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
X4
X4OP4y ago
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!)
Scot
Scot4y ago
Yup and those systems are extremely complex Even "last write wins" algorithms can have issues because of clock drift

Did you find this page helpful?