Make your App faster using Optimistic Updates

Make your App faster using Optimistic Updates

ยท

4 min read

Introduction

In this article, we will discuss what is an optimistic update and how to implement it in your React app to make it feel faster and more responsive, using React Query.

What's an optimistic update?

When doing operations that involve waiting for external services, like an API call that will mostly succeed, we proactively update the UI. This approach improves the user experience, making it better and smoother.

Use Case Example

Imagine you have a list of todos, and we have to add a delete action in the frontend to remove a todo.

Without optimistic update

When the users click on delete, we will:

  1. Send a request to the backend to delete the todo.

  2. Once we receive a response, refresh the todos list.

  3. the todo will be removed from the UI.

Here is a code example with React Query :

export default function useDeleteTodo() {
  // Get the query client instance for managing queries
  const queryClient = useQueryClient();

  // Use the useMutation hook to create the deleteTodo mutation
  const { isLoading: isDeleting, mutate } = useMutation({
    // Define the mutation function that will delete the todo
    mutationFn: (id) => deleteTodo(id),

    // Refetch todos after the success of todo deletion
    // to removed the todo from the UI
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: ["todos"],
      });
    },
  });

  // Return the loading state and mutate function
  return [isDeleting, mutate];
}

In this example, we use the useMutation hook, which takes:

  • mutationFn : It takes the async function to delete the todo in the backend.

  • onSuccess : it is called when the mutation is finished, and in this example, we refetch the todos list, so that the deleted todo is removed from the UI.

With an optimistic update

When the users click on delete, we will :

  1. Remove the todo from the UI (assuming successful backend deletion).

  2. Send a request to the backend to delete the todo.

  3. In case of failure, restore the list to its state before deletion.

Unlike the first approach, the deletion of the todo will look like it's instant since we won't wait for the API call response to update the UI.

The code with an optimistic update :


const queryKey = "todos";

export default function useDeleteTodo() {
  const queryClient = useQueryClient();

  const { isLoading: isDeleting, mutate } = useMutation({
    mutationFn: (id) => deleteTodo(id),
    // When mutate is called:
    onMutate: async (todoToDeleteId) => {
      // Cancel any outgoing refetches
      // so they don't overwrite our optimistic update
      await queryClient.cancelQueries(queryKey);

      // Snapshot the previous value
      const previousTodos = queryClient.getQueryData([queryKey]);

      // Optimistically update the local state by removing the todo
      queryClient.setQueryData([queryKey], (oldTodos) =>
        oldTodos.filter((todo) => todo.id != todoToDeleteId)
      );

      // Return a context object with the snapshotted value
      return { previousTodos };
    },
    // If the mutation fails
    // use the context returned from onMutate to roll back
    onError: (err, todoToDeleteId, context) => {
      queryClient.setQueryData([queryKey], context.previousTodos)
    },
  });

  return [isDeleting, mutate];
}

We added onMutate which will take the todoToDeleteId, it will be executed before the mutation is happening (in this case, delete todo).

  1. Cancel any outgoing fetches (so they don't overwrite our optimistic update).

  2. Take a snapshot of the previous todos.

  3. Remove the todo in the local state, which removes the todo from the UI.

  4. Returns the snapshot as a context (will be used later to roll back in case of failure).

We added an onError callback, which is called in case of failure. It utilizes the context returned to roll back to the old data.

Additionally, we removed the onSuccess callback since we have already removed the todo from the UI and won't need to refetch.

Next Steps

You may want to acknowledge the user about the success or failure of the deletion to enhance the user experience. For instance, by displaying a toast notification for both error and success scenarios.

Conclusion

In a nutshell, optimistic updates make your application feel faster. It's like a quick tweak to the user interface before the backend responds, ensuring a smoother user experience.

Consider adding optimistic updates to your code for a more visually responsive interface and the impression of smoother app performance.

Acknowledgment

Many Thanks to Ali Guedda for reviewing this article.

Thank you for reading, and happy coding!

ย