React Concurrent Features

Keep user interfaces responsive during heavy rendering operations using useTransition, startTransition, and useDeferredValue.

TL;DR

  1. Prioritize urgent user keystrokes above heavy background transitions.
  2. Track pending execution states automatically using the useTransition hook.
  3. Postpone expensive secondary re-renders with the non-blocking useDeferredValue API.

Transitions with useTransition

    useTransition Hook

    Mark state mutation as non-urgent background task.

    const [isPending, startTransition] = useTransition();
    const onTabChange = (nextTab: string) => {
      startTransition(() => {
        setActiveTab(nextTab);
      });
    };
    Pending Visual Indicator

    Dim UI or display spinner while transition runs.

    <CardView opacity={isPending ? 0.7 : 1}>
      {isPending && <Text>Updating list...</Text>}
      <TabContent tab={activeTab} />
    </CardView>
    Async Transition Handler

    Execute async operations inside startTransition.

    startTransition(async () => {
      await mutateBackend();
      setStep(2);
    });

Deferred Values with useDeferredValue

    useDeferredValue API

    Lag heavy component render behind fast input.

    export function SearchBox() {
      const [query, setQuery] = useState('');
      const deferredQuery = useDeferredValue(query);
      return (
        <CardView>
          <InputBase value={query} onChange={setQuery} />
          <HeavyList query={deferredQuery} />
        </CardView>
      );
    }
    Stale Content Detection

    Detect when deferred value is lagging behind input.

    const isStale = query !== deferredQuery;
    return <CardView opacity={isStale ? 0.5 : 1} />;
    Initial Value in React 19

    Supply initial deferred fallback in React 19.

    const val = useDeferredValue(input, 'initial');

Concurrent Rendering Priorities

    Urgent vs Transition

    Typing is urgent; rendering 1,000 cards is transition.

    // Urgent: keep typing responsive
    setQuery(text);
    // Transition: heavy graph filtering
    startTransition(() => setFiltered(items));
    Interrupted Rendering

    React discards outdated render if new input arrives.

    // If user types 'a' then 'b', rendering 'a' aborts
    const deferred = useDeferredValue(query);
    Concurrent Navigation

    Transition router pushes to avoid white flash.

    startTransition(() => {
      router.push('/heavy-analytics');
    });

Concurrency Pitfalls

    Controlled Input Lag

    Never wrap input text setters in transitions.

    // BAD: Input feels sticky and laggy
    startTransition(() => setText(e.target.value));
    Premature Concurrency

    Fix slow algorithms before reaching for concurrency.

    // Better: Paginate or virtualize first
    <VirtualList items={heavyList} />
    Impure Render Traps

    Concurrent React aborts renders; keep functions pure.

    // BAD: Mutating variables outside component body
    globalCounter += 1;

Tips

  1. Wrap expensive search list filtering in startTransition so text input typing remains buttery smooth without frame drops.
  2. Use useDeferredValue when filtering lists driven by parent props where you do not have direct access to the state updater.

Warnings

  1. Never wrap controlled input text value updates in startTransition; typing input state must always update urgently.
  2. Remember that useDeferredValue does not prevent rendering work; it merely yields thread control to urgent user events.

In Practice

FAQ