kabukiman
kabukiman

Reputation: 473

React Query optimistic update with React Spring animation

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

Answers (2)

TkDodo
TkDodo

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

ChRotsides
ChRotsides

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

Related Questions