useOptimistic Race Conditions Cause UI Flicker in Next.js 16

test_admin Beginner 5/10/2026 409 views 2 likes 2 min read

Handling asynchronous state updates in Next.js 14/15—and looking ahead to the upcoming changes in React 19/Next 16 patterns—often leads to a familiar trap: race conditions during optimistic updates. I’ve been dealing with this while building a real-time collaborative task board powered by useOptimistic and Server Actions.

The issue is straightforward but frustrating. An action starts, the UI updates optimistically, and then the user clicks three different items in quick succession. The server responses may arrive out of order. Since useOptimistic state follows the lifecycle of the pending action, a slow first request returning after a faster second request can restore stale state to the UI. That creates an unsettling “flicker,” with the item jumping back and forth.

The only reliable fix I found was adding client-side “request versioning” or a timestamp check inside the transition. Cursor helped me refactor this quickly, but the important part is tracking the sequence correctly.

// hooks/useStableUpdate.ts
import { useOptimistic, useTransition } from 'react';

export function useStableUpdate<T>(initialState: T, updateFn: (state: T, value: any) => T) {
  const [optimisticState, setOptimisticState] = useOptimistic(initialState, updateFn);
  const [isPending, startTransition] = useTransition();
  
  // Track the latest request ID to ignore stale server responses
  const lastRequestId = useRef(0);

  const execute = async (action: (id: number) => Promise<void>, payload: any) => {
    const currentId = ++lastRequestId.current;
    
    startTransition(async () => {
      // Optimistic update happens immediately
      setOptimisticState(payload);
      
      try {
        await action(currentId);
        // If a newer request has already started, we don't manually 
        // trigger a refresh here to avoid fighting with the server state
      } catch (e) {
        console.error("Update failed", e);
      }
    });
  };

  return { optimisticState, execute, isPending };
}

A major gotcha with useOptimistic is that it does not “cancel” the previous pending state when a new transition begins; it layers the states. During complex array operations, such as reordering a list, the optimistic state can become desynchronized from the actual server state if the keys are not handled correctly.

To improve this in Cursor, I stopped asking it to “fix the bug” and began supplying the specific React 19 RFC docs through the .cursorrules file. This stops the AI from recommending old useEffect cleanup patterns, which are now essentially anti-patterns with transitions.

My current configuration for these complex hooks:

  • Rules file: Explicitly instruct Cursor to prioritize useTransition and useOptimistic over manual useState loading flags.
  • Prompting: Use “Refactor this to be race‑condition proof using a sequence counter” rather than “Make this work.”
  • Verification: Always check whether the AI has accidentally added await outside the startTransition block, which breaks optimistic behavior.
useOptimistic Race Conditions Cause UI Flicker in Next.js 16

The productivity improvement is substantial. Instead of writing 50 lines of useEffect and isLoading boilerplate, I now have a 15‑line hook that prevents the UI from snapping back. The key insight is that the server remains the source of truth, while the client needs a “sequence number” to determine which result is the most recent.

All Replies (0)

Want a live back-and-forth? Join the global AI chat room — login to talk.

No replies yet — be the first!

Write a Reply

Markdown supported