Optimistic UI Rollback: Issues and Solution in React Query (React Native)
Problem Encountered
When implementing optimistic UI for movie swiping, we wanted to:
- Remove the card from the stack immediately on swipe (optimistic update)
- If the API call fails, restore the swiped card to the stack (rollback)
Challenge:
- How do we know which card to restore if the mutation fails, especially if the user has swiped multiple times quickly?
- How do we pass the swiped movie data to the error handler in a robust, React Query-compatible way?
Issues Faced
- React Query's
useMutationdoes not support avariablesorcontextproperty in themutateoptions directly. - The
onErrorcallback receives the mutation variables as the second argument, but not the full movie object unless you pass it yourself. - If you try to use a closure variable (like
topMovie), it may be stale or incorrect if the user swipes quickly.
Solution: Use the context Pattern
React Query's mutate function allows you to pass a context object via the onMutate callback. This context is then available in onError and onSettled.
Pattern:
- In
onMutate, return the swiped movie as context. - In
onError, use the context to restore the card.
Example:
const swipeMutation = useMutation({
mutationFn: async (swipeData) => { /* ... */ },
onMutate: (swipeData) => {
// Find the movie being swiped
const swipedMovie = cardStack[cardStack.length - 1]
setCardStack((prev) => prev.slice(0, -1)) // Optimistically remove
return { swipedMovie }
},
onError: (error, variables, context) => {
setError(error.message || 'Unknown error')
if (context?.swipedMovie) {
setCardStack((prev) => [...prev, context.swipedMovie]) // Restore
}
},
onSuccess: () => setError(null),
})
const handleSwipe = (direction) => {
// ...determine liked/skipped/score...
swipeMutation.mutate({ movie_id, liked, skipped, score })
}
Why This Way?
- Robust: The context pattern ensures you always restore the correct card, even if the user swipes quickly or out of order.
- Idiomatic: This is the recommended approach in React Query for optimistic updates and rollback.
- Decoupled: You don't rely on closure variables or global state, which can be error-prone.
References
This doc explains the technical reasoning and solution for robust optimistic UI rollback in our React Native swiping stack.