5 min readReact Query · Part 9 of 12

Improve user experience with optimistic update

Hey Folks, Sometimes to improve the user experience you can decide to bet on the success of your...

#react#reactquery#reacthooks
Luca Del PuppoLuca Del Puppo
Article Series · Part 9 of 12React Query
View full series (12)
  1. #8Previous
    Keys & Prefetching
  2. #9Reading now
    Improve user experience with optimistic update
  3. #10Up next
    Abort Request
Browse all 12 parts in this series
  1. #1useQuery
  2. #2useMutation
  3. #3useIsFetching & useIsMutation
  4. #4DevTools
  5. #5Authentication Flow
  6. #6Filter Your Data
  7. #7Enable Query
  8. #8Keys & Prefetching
  9. #9Improve user experience with optimistic updateCurrent
  10. #10Abort Request
  11. #11Paginated List
  12. #12Infinite Queries

Hey Folks,

Sometimes to improve the user experience you can decide to bet on the success of your code, so you can assume that the code will go in the right way to make your application faster in the eyes of your users. This approach is called Optimistic Update and can be handled in some lines of code using react query with the useMutation hook. Yes, this approach is like a bet with the code but it’s also important to keep in mind to handle the errors in case of failure.

To handle the optimistic update you have to handle an useMutation hook in your codebase. This hook exposes three events to handle to perform the optimistic update: onMutate, onSuccess and the onError.

The onMutate event is called immediately when your code calls the useMutation hook. This event is used to create a snapshot of the current state before moving forward and updating the state with the new values. It’s important to save the previous state because it permits you in case of failure to restore it in the future if needed.

The onSuccess event is called in case of success of the mutation. If your code jumps in this event, you are safe and everything has gone in the right way.

The onError event is called in case of failure of the mutation. If your code is inside of this event unfortunately something went wrong. In this case, you must handle the restoration of the state to the previous one and notify the user that something went wrong.

To help you to understand better the case let’s see an example

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useSnackbar } from 'notistack';

const editTodoRequest = async (todo: Todo): Promise<Todo> => {
  await new Promise((resolve) => setTimeout(resolve, 3000));
  const response = await fetch(`api/tasks/${todo.id}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(todo),
  });
  if (!response.ok) {
    throw new ResponseError(`Failed to edit todo with id ${todo.id}`, response);
  }
  return await response.json();
};

type UseEditTodo = (todo: Todo) => void;

export const useEditTodo = (): UseEditTodo => {
  const { enqueueSnackbar, closeSnackbar } = useSnackbar();
  const client = useQueryClient();
  const { mutate: editTodo } = useMutation<
    Todo,
    unknown,
    Todo,
    {
      oldTodos?: Todo[];
      oldTodo?: Todo;
      messageKey: string | number;
    }
  >(editTodoRequest, {
    onMutate: (todo) => {
      const messageKey = enqueueSnackbar('Todo edited', {
        variant: 'success',
      });
      const oldTodos = client.getQueryData<Todo[]>([QUERY_KEY.todos]);
      const oldTodo = client.getQueryData<Todo>([QUERY_KEY.todos, todo.id]);
      client.setQueryData([QUERY_KEY.todos, todo.id], todo);
      client.setQueryData<Todo[]>([QUERY_KEY.todos], (oldTodos) =>
        oldTodos?.map((oldTodo) => (oldTodo.id === todo.id ? todo : oldTodo))
      );

      return {
        oldTodos,
        oldTodo,
        messageKey,
      };
    },
    onSuccess: () => {
      client.invalidateQueries([QUERY_KEY.todos]);
    },
    onError: (error, todo, ctx) => {
      if (!ctx) return;
      const { oldTodos, oldTodo, messageKey } = ctx;
      closeSnackbar(messageKey);
      const errorMessage = mapError(error);
      enqueueSnackbar(
        `Ops! There was an error on editing todo: ${errorMessage}`,
        {
          variant: 'error',
        }
      );
      client.setQueryData([QUERY_KEY.todos, todo.id], oldTodo);
      client.setQueryData<Todo[]>([QUERY_KEY.todos], oldTodos);
    },
  });

  return editTodo;
};

As you can notice, in the mutate event, the code creates a toast for the user, which indicates that everything has gone in the right way and after that, the code takes the current state, returns it and update the state with the new values. The return is important because it permits getting this data in the onError event in case of failure. The onError event, as you can notice, handles the restoration of the state, removes the success toast and shows a new one with the error message. This part is crucial if you want to handle an optimistic update because it gives feedback to the user in case of failure.

Last but not least, you can also handle the onSuccess event if you want to invalidate a query after the mutation or for some other motivation. The onSuccess is not required always but depends on your case.

Ok, I think now you have an idea of how the optimistic update works in react query, but if you want to dive into it check out my youtube video too.

React Query - Optimistic UpdateClick to play inline · youtube-nocookie.com (no requests until played)
Watch on YouTube

I think thats all from this article; I hope you enjoyed this content!

See you soon folks
Bye Bye 👋

p.s. you can find the code of the video in the GitHub repository

Photo by Rahul Mishra on Unsplash

🎬 Watch / View Embedded Resource: https://dev.to/puppo

Luca Del Puppo

Written by Luca Del Puppo

Run, Code & Learn

AI Native Engineer at Nearform based in Povegliano (TV), Italy. Google Developer Expert in Web Technologies, GitKraken Ambassador, and Codemotion Ambassador.