React: Optimistic UI Updates Without Inconsistent Data

Introduction

Users expect an interface to respond immediately when they like a post, mark a task complete, or update a small setting. Waiting for a server response before showing any change can make an application feel sluggish.

Optimistic UI updates improve perceived responsiveness by showing the expected result before the server confirms it. The challenge is recovering correctly when the request fails or another user changes the same data.

1. Understand the optimistic update flow

A typical optimistic interaction has four stages:

  1. Record the current state.
  2. Apply the expected change immediately.
  3. Send the mutation to the server.
  4. Confirm the result or recover if the request fails.

This is best suited to operations where the expected outcome is reasonably predictable and the application can recover from rejection.

2. Use React’s optimistic state support

React provides useOptimistic for representing temporary optimistic state during an Action:

import { useOptimistic, useState, startTransition } from "react";

function LikeButton({ initialLiked, saveLike }) {
  const [liked, setLiked] = useState(initialLiked);

  const [optimisticLiked, setOptimisticLiked] =
    useOptimistic(liked);

  function handleClick() {
    startTransition(async () => {
      const nextValue = !liked;
      setOptimisticLiked(nextValue);

      try {
        await saveLike(nextValue);
        setLiked(nextValue);
      } catch {
        // The authoritative state remains unchanged.
        // The optimistic state reverts when the Action ends.
      }
    });
  }

  return (
    <button onClick={handleClick}>
      {optimisticLiked ? "Unlike" : "Like"}
    </button>
  );
}

This is a simplified example that assumes saveLike returns a promise and that the server accepts the requested value. Production code should also handle rapid repeated clicks, server responses that return authoritative state, and visible error feedback.

3. Consider concurrent actions

A simple toggle becomes more complicated when users click repeatedly before the first request finishes.

Two requests may arrive in a different order than they were sent. A response from an older request could then overwrite a newer choice.

Possible safeguards include disabling repeated submission temporarily, using request identifiers, sending the desired final value rather than a blind toggle, or having the server return an authoritative version.

Choose the simplest strategy that matches the interaction’s risk and expected traffic.

4. Avoid optimistic updates for every operation

Optimism is not appropriate when the result is highly uncertain or the consequences are serious.

For example, a payment should not be presented as completed merely because a request was sent. The interface can show that the payment is being processed, but it should distinguish that state from confirmed success.

The same principle applies to deleting important data or performing irreversible actions.

5. Test rollback and failure behavior

Test rejected requests, slow responses, duplicate clicks, and stale server data. Verify that the interface does not remain in an optimistic state after the operation fails.

Also ensure that errors are communicated accessibly and that users can retry when appropriate.

Conclusion

Optimistic updates make low-risk interactions feel faster, but they require a clear recovery strategy. Use them where the expected result is predictable, reconcile with the server’s authoritative response, and test failure and concurrency scenarios.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *