Performance

useTransition

Marks a state update as non-urgent, allowing the UI to remain responsive during the transition. Returns a pending flag and a function to wrap low-priority updates.

Signature

TypeScript
const [isPending, startTransition] = useTransition()

Return Value

A tuple of [isPending, startTransition]. isPending is true while the transition is rendering. startTransition accepts a callback containing state updates to defer.

Examples

Tab Navigation
import { useState, useTransition } from 'react';

function TabContainer() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  function selectTab(nextTab: string) {
    startTransition(() => {
      setTab(nextTab);
    });
  }

  return (
    <div>
      <nav>
        {['home', 'posts', 'settings'].map(t => (
          <button
            key={t}
            onClick={() => selectTab(t)}
            style={{ fontWeight: tab === t ? 'bold' : 'normal' }}
          >
            {t}
          </button>
        ))}
      </nav>
      {isPending && <div>Loading...</div>}
      <TabContent tab={tab} />
    </div>
  );
}

function TabContent({ tab }: { tab: string }) {
  // Simulate expensive render
  const items = Array.from({ length: 1000 }, (_, i) => <p key={i}>{tab} item {i}</p>);
  return <div>{items}</div>;
}
Filter with Transition
import { useState, useTransition } from 'react';

function FilteredProducts({ products }: { products: { name: string; category: string }[] }) {
  const [category, setCategory] = useState('all');
  const [isPending, startTransition] = useTransition();

  const filtered = category === 'all'
    ? products
    : products.filter(p => p.category === category);

  return (
    <div>
      <select
        value={category}
        onChange={e => {
          startTransition(() => setCategory(e.target.value));
        }}
      >
        <option value="all">All</option>
        <option value="electronics">Electronics</option>
        <option value="clothing">Clothing</option>
      </select>
      <div style={{ opacity: isPending ? 0.7 : 1 }}>
        {filtered.map(p => <div key={p.name}>{p.name}</div>)}
      </div>
    </div>
  );
}
Async Transition (React 19+)
import { useState, useTransition } from 'react';

function SubmitForm() {
  const [data, setData] = useState<any>(null);
  const [isPending, startTransition] = useTransition();

  async function handleSubmit(formData: FormData) {
    startTransition(async () => {
      const res = await fetch('/api/submit', {
        method: 'POST',
        body: formData,
      });
      const json = await res.json();
      setData(json);
    });
  }

  return (
    <form action={handleSubmit}>
      <input name="email" type="email" required />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Submitting...' : 'Submit'}
      </button>
      {data && <p>Success: {JSON.stringify(data)}</p>}
    </form>
  );
}

Common Pitfalls

!

Wrapping synchronous, cheap state updates in startTransition — there's no benefit unless the resulting render is expensive.

!

Expecting startTransition to work like setTimeout — it doesn't delay, it schedules at a lower priority.

!

Putting non-state-setting code inside startTransition — only setState calls inside the callback are deferred.

!

Not using the isPending flag to provide visual feedback, leaving users unsure if their action registered.

Understanding useTransition

useTransition is a hook for marking state updates as non-urgent, enabling React's concurrent features to keep the UI responsive during expensive re-renders. Unlike useDeferredValue which defers a value, useTransition defers the entire state update and provides a pending flag you can use to show loading indicators.

The startTransition function wraps one or more setState calls. React treats these updates as low-priority — if a higher-priority update comes in (like a user keystroke), React can interrupt the transition render and restart it with the newer data. This interruptible rendering is the core of React's concurrent model and is what prevents the UI from freezing during heavy renders.

The isPending boolean is true from the moment you call startTransition until the transition render completes. This gives you a built-in mechanism for loading states without managing a separate loading variable. You can use it to dim the current content, show a spinner, or disable buttons — providing clear feedback that the UI is updating.

In React 19+, startTransition supports async functions, enabling a powerful pattern for form submissions and data mutations. You can await a fetch call inside startTransition, and isPending will remain true until the entire async operation completes and the state updates are rendered. This eliminates the need for manual loading state management in many common patterns. Combined with useActionState and useOptimistic, transitions form the foundation of React 19's approach to data mutations.

Related Hooks

More Performance Hooks

Explore All React Hooks

Browse our complete reference of 19 React hooks with signatures, examples, pitfalls, and in-depth explanations.