Reputation: 473
I'm using react query mutation to create an object and update UI optimistically
const queryClient = useQueryClient()
useMutation({
mutationFn: updateTodo,
onMutate: async newTodo => {
await queryClient.cancelQueries({ queryKey: ['todos'] })
const previousTodos = queryClient.getQueryData(['todos'])
// Optimistically update to the new value
queryClient.setQueryData(['todos'], old => [...old, newTodo])
return { previousTodos }
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context.previousTodos)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
New in-memory todo item have some random ID and displayed in UI with React Spring animation. Then i get response from server with success confirmation and real todo item ID. My application replaces and reanimates UI element and this is the problem. Optimistic update is must-have feature, but i don't know how to stop this behaviour. Need help
Upvotes: 4
Views: 863
Reputation: 28763
The optimistic update looks fine from a react-query perspective, there's nothing to improve on that front.
I guess react-spring reanimates the DOM node because you use the id
as key
when rendering the todos, but it's hard to say without seeing the actual animation code.
If that is indeed the case, you could try to decouple the actual database ids and ids used for rendering. For example, you could store and return the randomly created id as an additional field, like renderId
, so that your todos have the structure of:
{ id: 'id-1', renderId: 'random-string-1', title: 'my-todo-1', done: false }
{ id: 'id-2', renderId: 'random-string-2', title: 'my-todo-2', done: true }
when you create a new todo, you set both id
and renderId
to the random string when doing the optimistic update:
{ id: 'random-string-3', renderId: 'random-string-3', title: 'my-optimistic-todo', done: false }
then, when it comes back from the db after the invalidation, it will be:
{ id: 'id-3', renderId: 'random-string-3', title: 'my-optimistic-todo', done: false }
that means the renderId
will always be consistent, so replacing todo with the real value after the optimistic update has been performed should not re-trigger the animation if you use randomId
as key
.
if you cannot amend the backend schema, you could also generate the renderId on the client, inside the queryFn, if there is no entry in the cache for your current key:
const useTodoQuery = (id) => {
const queryClient = useQueryClient()
return useQuery({
queryKey: ['todos', id],
queryFn: async ({ queryKey }) => {
const todo = await fetchTodo(id)
const renderId = queryClient.getQueryData(queryKey)?.renderId
return {
...todo,
renderId: renderId ?? generateRenderId()
}
})
}
then, if you have already created the renderId
during the optimistic update process, you wouldn't create a new one when the queryFn runs.
Upvotes: 0
Reputation: 11
You can use the 'onSuccess' callback function to update the query data.
const queryClient = useQueryClient()
useMutation({
mutationFn: updateTodo,
onMutate: async newTodo => {
await queryClient.cancelQueries({ queryKey: ['todos'] })
const previousTodos = queryClient.getQueryData(['todos'])
// Optimistically update to the new value
queryClient.setQueryData(['todos'], old => [...old, newTodo])
return { previousTodos }
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context.previousTodos)
},
onSuccess: (data, newTodo) => {
// Update the query data with the real todo item ID from the server response
queryClient.setQueryData(['todos'], old => old.map(todo => todo.id === newTodo.id ? data.todo : todo))
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},})
Upvotes: 0