React|Built-in Hook Mastery

useTransition (Async Support) vs useDeferredValue: The Completion of Concurrent Rendering

4
useTransition (Async Support) vs useDeferredValue: The Completion of Concurrent Rendering

There is a fatal flaw in the search feature Chris built.

Every time the user types a search query, the app filters 10,000 items and repaints the list.

typescript
// โŒ The Laggy Search Bar
function SearchApp() {
  const [query, setQuery] = useState('');
  
  const handleChange = (e) => {
    // Update input value (Urgent)
    setQuery(e.target.value); 
    // List filtering logic executes, hogging the main thread -> Input stutters
  };

  return (
    <div>
      <input value={query} onChange={handleChange} />
      <HeavyList query={query} />
    </div>
  );
}

The screen stutters with every keystroke. Chris thought, "Should I use Debounce?" but since Debounce only reacts after the input stops, the search experience feels sluggish.

The Concurrency hooks introduced in React 18 solve this problem fundamentally. The core idea is "Process urgent updates immediately, and handle heavy updates later (Interruptible)."

Today, we will compare the two tools that perform this magic, useTransition and useDeferredValue, and even look at the async support features added in React 19.

1. useTransition: "Do the urgent things first"

useTransition is a hook that marks state updates as "Non-urgent."

In our situation:

  • Urgent: Letters appearing in the input box (Must be visible to the user immediately).
  • Less Urgent: The list below being filtered (It doesn't matter if it appears 0.1 seconds late).
  • Chris decided to wrap the list update with startTransition.

    typescript
    // โœ… Applying useTransition
    import { useState, useTransition } from 'react';
    
    function SearchApp() {
      const [query, setQuery] = useState('');
      const [listQuery, setListQuery] = useState('');
      const [isPending, startTransition] = useTransition();
    
      const handleChange = (e) => {
        const value = e.target.value;
        
        // 1. Urgent Update: Input reacts immediately
        setQuery(value);
    
        // 2. Transition Update: List filtering is pushed to the back burner
        startTransition(() => {
          setListQuery(value);
        });
      };
    
      return (
        <div>
          <input value={query} onChange={handleChange} />
          
          {/* Loading state can be shown using isPending */}
          {isPending && <span className="spinner">Filtering...</span>}
          
          {/* The list watches listQuery */}
          <HeavyList query={listQuery} />
        </div>
      );
    }

    Now, when the user types quickly, React busily processes only setQuery and skips the work inside startTransition. Only when the typing stops does it render the list with the latest value. The input box never stutters.

    Evolution in React 19: Async Transitions

    Starting from React 19, you can put Async functions (Async/Await) inside startTransition!

    This is also the underlying principle of useActionState that we learned earlier.

    typescript
    // React 19: Handling async tasks with Transition
    const handleClick = () => {
      startTransition(async () => {
        // isPending automatically becomes true until the API call finishes.
        await updateProfile(name);
      });
    };

    Now, you can control the UI during asynchronous operations without creating a separate isLoading state.

    2. useDeferredValue: "I'll use this value later"

    useDeferredValue shares the same goal as useTransition, but its usage differs.

    While useTransition wraps the state updating function (Setter), useDeferredValue wraps the value itself.

    It is useful when you want to defer values received via Props.

    typescript
    // โœ… Applying useDeferredValue
    import { useState, useDeferredValue } from 'react';
    
    function SearchApp() {
      const [query, setQuery] = useState('');
      
      // query changes immediately, but deferredQuery changes when React has time.
      const deferredQuery = useDeferredValue(query);
    
      return (
        <div>
          {/* The input uses the original query for immediate reaction */}
          <input value={query} onChange={e => setQuery(e.target.value)} />
          
          {/* The list uses the deferred value */}
          {/* It continues to show the old list until deferredQuery updates */}
          <HeavyList query={deferredQuery} />
        </div>
      );
    }

    The code is much simpler. It is extremely useful in situations where you cannot use startTransition (e.g., when using a value provided by a library as is).

    3. Which One Should I Use? (Comparison)

    Both hooks share the same goal of "Adjusting Rendering Priority."

    FeatureuseTransitionuseDeferredValue
    Control TargetState Update Function (setState)Value (props, variables)
    Usage LocationWhere state changes (Event Handler)Where value is used (Child Component, Props)
    ProsProvides isPending state for loading UIConcise code, good for useEffect dependency arrays
    Analogy"Process this task slowly" (Command)"This value can be less fresh" (Data)

    Chris's Selection Guide:

  • Can I call setState directly? -> useTransition (Good because it handles loading indicators too).
  • Did I receive Props from a parent and it causes heavy rendering? -> useDeferredValue.
  • 4. How is it different from Debounce?

  • Debounce: Uses setTimeout to delay the task entirely. Nothing happens for 0.5 seconds. (Time is wasted).
  • Concurrency: Starts the task, but if a more urgent task (keystroke) occurs, it Interrupts and yields. It utilizes the CPU fully without wasted time to maximize user responsiveness.
  • Therefore, for rendering optimization, Concurrency hooks are much more user-friendly (UX) than Debounce. (However, if the goal is to reduce API requests, Debounce is still necessary.)

    Key Takeaways

  • Problem: If heavy rendering tasks block the main thread, essential UI elements like input fields stutter.
  • useTransition: Lowers the priority of state updates. You can track progress via isPending, and React 19 supports async functions.
  • useDeferredValue: Defers the update of a value. Useful when using data received via Props for rendering.
  • Conclusion: Using these instead of Debounce provides a smooth, stutter-free UI experience.

  • We've now conquered rendering performance.

    The final step in Built-in Hook Mastery is "Synchronization with the External World."

    Are you aware of the Tearing issue that occurs when subscribing to window.resize events or creating external stores like Redux/Zustand via useEffect?

    Continuing in: "useSyncExternalStore & useId: Essential Hooks for Library Developers and SSR."

    ๐Ÿ”— References

  • React Docs - useTransition
  • React Docs - useDeferredValue
  • Patterns for concurrency
  • Comments (0)

    0/1000 characters
    Loading comments...