4

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

Nil
  • 1,209
  • 9
  • 20
kabukiman
  • 185
  • 14
  • Maybe you can try setting up the element's presence animation based on the `isLoading` boolean returned by `useMutation`. – ivanatias Dec 31 '22 at 06:50

2 Answers2

0

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'] })
},})
ChRotsides
  • 11
  • 1
  • There is no problem with updating data. There is a problem that html element reanimates (React Spring) when i update data with real ID. My todo element fades in with temporary data (optimistic), then fetches from server and fades in again with what, to user, looks like the same element – kabukiman Jan 01 '23 at 09:08
0

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.

TkDodo
  • 20,449
  • 3
  • 50
  • 65
  • 1
    Your assumption is correct. I use ID as key with absolute basic useTransition React Spring example . Your solution, most definitely, will work, but its impossible, in my position, to update database scheme. I've assumed i missed something on frontend side that will help solve this – kabukiman Jan 01 '23 at 20:24
  • I don't think there is. If the key changes on a react component, it will treat it as "new", so the animation will run again. You have to convince react that the element you render optimistically is "the same" as the element that you receive when the mutation succeeds, even though it has a different `id` then. You could also always create a stable renderId inside the queryFn, if there is no entry in the cache for the current key. I've amended the answer with that option and an example implementation. – TkDodo Jan 03 '23 at 18:34