Open navigation
Back to Articles

May 2026 · 2 min read

Stop Showing Loading Spinners for Safe Mutations

Loading spinners are a failure of UX design when simple toggle actions are 99% guaranteed to succeed. Here is how to use React 19 useOptimistic for instant interfaces.

Editorial cover for stop showing loading spinners safe mutations

The high friction of defensive loading indicators

I spent far too long dealing with layout shift caused by loading spinners on simple toggle buttons. When users like a post or check off a todo item, they do not want to wait 200 milliseconds for the database to confirm. Waiting for an API response before updating the interface makes a web application feel slow and unresponsive.

Forcing a user to wait for simple actions introduces unnecessary friction. When a user triggers an action, the backend request runs in the background. Assuming success upfront allows the UI to update immediately. This approach completely bypasses the need for defensive, blocking visual states.

Knowing when to fallback to standard pending states

Optimistic updates are not a universal solution for every frontend interaction. Destructive actions, such as permanent deletions without an undo option, should never show instant success. If a user has already mentally confirmed a deletion, rolling back that action after a server error is highly disruptive.

Similarly, financial transactions and inventory counts are poor fits for this pattern. Showing a temporarily incorrect balance could lead a user to place an invalid order. If your system depends solely on server-side validations with no client-side checks, a sudden flash-then-revert sequence feels incredibly jarring.

Implementing instant toggle state with React 19

In my own projects, transitioning to React 19 made writing these instant updates incredibly straightforward. By pairing a server mutation with the useOptimistic hook, we can let the interface react instantly. If the network call fails or the server throws an error, React handles the rollback gracefully.

tsx
"use client";
import { useOptimistic, useTransition } from "react";
import { toggleLikeAction } from "./actions";

type LikeState = { liked: boolean; count: number };

export function LikeButton({ postId, initialState }: { postId: string; initialState: LikeState }) {
  const [isPending, startTransition] = useTransition();
  const [optimisticState, setOptimisticState] = useOptimistic(
    initialState,
    (current) => ({
      liked: !current.liked,
      count: current.liked ? current.count - 1 : current.count + 1
    })
  );

  return (
    <button
      onClick={() => {
        startTransition(async () => {
          setOptimisticState(null);
          try {
            await toggleLikeAction(postId);
          } catch (error) {
          }
        });
      }}
    >
      {optimisticState.liked ? "Liked" : "Like"} ({optimisticState.count})
    </button>
  );
}
nextjsreact-19frontend-uxoptimistic-ui

Sources

  1. useOptimistic – React
  2. Best Practices for Speeding Up Your Web Site
  3. Performance Best Practices for Web Apps | Design and Develop Vega Apps
  4. React 19 useOptimistic - Codefinity
  5. React useOptimistic: Production Patterns for Instant UI Updates
  6. Optimistic UI with React 19's useOptimistic Hook | Amanpreet Kaur posted on the topic | LinkedIn
Press ? for keyboard shortcuts