Enhancing React Apps: Optimistic Updates with Tanstack Query Explained

Enhancing React Apps: Optimistic Updates with Tanstack Query Explained

"Boost Your App's Responsiveness with React Query's Optimistic Updates"

Optimistic updates are a game-changing approach to improving user experience in modern web applications. By updating the user interface (UI) immediately, instead of waiting for the server's response, you can make your app feel faster and more responsive. In this article, we'll explore optimistic updates in detail, explain how they work, and show you how to implement them using React Query.


What Are Optimistic Updates?

Optimistic updates are a UI-first approach to handling data changes. Instead of waiting for a server response, the app assumes the operation will succeed and updates the UI accordingly. If the server confirms success, the UI remains as is. If the server reports an error, the app rolls back the UI to its previous state.

Analogy to Understand Optimistic Updates

Imagine you're ordering food at a restaurant:

  1. Without Optimistic Updates:

    • You place an order.

    • The waiter disappears to check with the kitchen.

    • You only see your food on the table after it’s confirmed and cooked.

(This feels slow because you see no progress until the action is complete.)

  1. With Optimistic Updates:

    • As soon as you place your order, the waiter puts a placeholder dish on your table, saying, “Your order is on the way!”

    • If the kitchen confirms, the placeholder is replaced by real food.

    • If something goes wrong (e.g., the dish is unavailable), the waiter removes the placeholder and apologizes.

(This feels faster and smoother because you get immediate feedback.)

In web apps, the "placeholder dish" is an immediate update to the UI that reflects the user’s action.


How Optimistic Updates Work

The process can be broken down into four steps:

  1. Trigger the Optimistic Update:

    • Temporarily update the UI as if the action succeeded.

    • Save the previous state in case you need to roll back.

  2. Make the Server Request:

    • Send the actual request to the server to perform the action.
  3. Handle Success:

    • If the server confirms success, no further action is needed.
  4. Handle Failure:

    • If the server reports an error, roll back the UI to the previous state and optionally show an error message.

Why Use Optimistic Updates?

  1. Improved User Experience:

    • Users see immediate feedback, making the app feel faster and more responsive.
  2. Reduced Perceived Latency:

    • The time it takes for the server to respond feels negligible to the user.
  3. Seamless Interactions:

    • Optimistic updates reduce the feeling of "waiting" for actions to complete.

Implementing Optimistic Updates with React Query

React Query makes implementing optimistic updates simple and efficient. It provides a useMutation hook with built-in support for:

  • Temporarily updating the cache.

  • Rolling back changes on failure.

  • Refreshing data after the mutation completes.

Here’s a practical example to illustrate.

Scenario: Snippet Upload Feature

In this example, users can upload code snippets with fields like title, description, language, and code. Optimistic updates will ensure the snippet appears in the list immediately after submission.

1. Set Up the Mutation

Use React Query’s useMutation to handle the snippet upload process:

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

const useAddSnippet = () => {
  const queryClient = useQueryClient();

  return useMutation({
    // Function to call your API
    mutationFn: async (snippet) => {
      const { data } = await axios.post('/api/snippets', snippet);
      return data;
    },

    // Optimistic Update
    onMutate: async (newSnippet) => {
      // Cancel any ongoing fetches for the "snippets" query
      await queryClient.cancelQueries(['snippets']);

      // Get the current state of the snippets
      const previousSnippets = queryClient.getQueryData(['snippets']);

      // Update the cache with the new snippet (optimistic update)
      queryClient.setQueryData(['snippets'], (oldSnippets = []) => [
        ...oldSnippets,
        { id: Date.now(), ...newSnippet }, // Temporary snippet with a fake ID
      ]);

      // Return a rollback function in case of failure
      return { previousSnippets };
    },

    // Rollback on Failure
    onError: (err, newSnippet, context) => {
      queryClient.setQueryData(['snippets'], context.previousSnippets);
    },

    // Sync Data on Success or Failure
    onSettled: () => {
      queryClient.invalidateQueries(['snippets']);
    },
  });
};

2. Use the Mutation in a Component

Here’s how to use the useAddSnippet hook in a component:

import React, { useState } from 'react';
import MonacoEditor from '@monaco-editor/react';
import { useAddSnippet } from '../hooks/useAddSnippet';

const SnippetUploader = () => {
  const [snippet, setSnippet] = useState({
    title: '',
    description: '',
    language: 'javascript',
    code: '',
  });

  const addSnippet = useAddSnippet();

  const handleSubmit = () => {
    addSnippet.mutate(snippet, {
      onSuccess: () => {
        alert('Snippet saved!');
        setSnippet({ title: '', description: '', language: 'javascript', code: '' });
      },
    });
  };

  return (
    <div>
      <input
        type="text"
        placeholder="Title"
        value={snippet.title}
        onChange={(e) => setSnippet({ ...snippet, title: e.target.value })}
      />
      <textarea
        placeholder="Description"
        value={snippet.description}
        onChange={(e) => setSnippet({ ...snippet, description: e.target.value })}
      />
      <MonacoEditor
        height="300px"
        language={snippet.language}
        value={snippet.code}
        onChange={(code) => setSnippet({ ...snippet, code })}
      />
      <button onClick={handleSubmit} disabled={addSnippet.isLoading}>
        {addSnippet.isLoading ? 'Saving...' : 'Save Snippet'}
      </button>
    </div>
  );
};

export default SnippetUploader;

3. How It Feels to the User

  1. The user clicks "Save Snippet."

  2. The snippet instantly appears in the list (even though the server hasn’t responded yet).

  3. If the server responds with success, nothing changes (the UI is already correct).

  4. If the server responds with an error, the snippet is removed, and an error message is shown.


Benefits of Optimistic Updates

  • Immediate Feedback: Users don’t feel like they’re waiting.

  • Improved Perceived Performance: Actions feel instantaneous.

  • Better UX: Creates a smooth and seamless experience.


Final Thoughts

Optimistic updates are an essential technique for creating fast, responsive, and user-friendly web apps. By leveraging tools like React Query, you can implement them with minimal effort while maintaining robust error handling and cache management.

If you’re building a feature where users interact with data—whether it’s creating, updating, or deleting—optimistic updates can make your app stand out.

Have questions or want to share your experience with optimistic updates? Let’s discuss in the comments below!