startTransition

Marks state updates as non-blocking Transitions so urgent input stays responsive.

Signature
startTransition(action)
  • Added in React 18.0.0

Parameters

ParameterTypeDescription
$action() => void | Promise<void>A function that updates some state by calling one or more set functions. React calls action immediately with no parameters and marks all state updates scheduled synchronously during that call as Transitions. Any async calls awaited inside action are included in the Transition, but set functions called after an await currently need to be wrapped in an additional startTransition.

Return value

voidstartTransition does not return anything.

Changelog

  • PHP 19.0.0`action` may be an async function; awaited work is part of the Transition, and errors thrown inside it surface to the nearest error boundary. `set` calls after an `await` still need their own `startTransition`.
  • PHP 18.0.0Introduced as the version of `useTransition` without pending feedback.

startTransition tells React that the state updates made inside the callback are not urgent. React renders them in the background, keeps the previous UI interactive while it works, and throws that work away and restarts if a more urgent update — a keystroke, a click — arrives first.

When to reach for it

Reach for it when one interaction schedules two updates of different urgency: the controlled input's value must land on the next frame, while the 5,000-row list it filters can lag by a beat. Update the input normally and wrap the expensive derived update in startTransition. Tab switches into an expensive panel and navigations are the other classic cases.

startTransition or useTransition?

Use useTransition when you want a pending flag to grey out the panel or show a spinner. Use the standalone startTransition when there is no pending UI, or when you must start a Transition outside a component — in a store subscription, a router, an event handler in a non-React module.

What trips people up

Only updates scheduled synchronously inside the callback are marked. Anything in a setTimeout, a .then(), or after an await has already left the Transition scope and needs its own startTransition. And a Transition cannot control a text input's value — that update must stay urgent, or typing will feel broken.

Example

Try it yourself
Loading editor…
Loading React…

When to use it

  • Keep a search input responsive while the results list it filters re-renders thousands of rows.
  • Switch to a heavy tab without freezing the tab bar mid-click.
  • Let a router mark a navigation as non-urgent so the current page stays interactive until the next one is ready.

Frequently asked questions

What is the difference between startTransition and useTransition?

useTransition returns an isPending boolean alongside the same start function; the standalone startTransition returns nothing and cannot tell you whether a Transition is in flight. Use the standalone form when you have no pending UI to show or when you need to start a Transition outside a component, such as in a router or a store.

Why is my state update after await not treated as a Transition?

Because React only marks updates scheduled synchronously while the action function runs, and the code after an await runs in a later microtask. Wrap those set calls in another startTransition. React documents this as a current limitation it intends to fix.

Can I use startTransition for a controlled text input?

No — the docs state that Transition updates cannot be used to control text inputs. Update the input's value with a normal urgent update and put only the expensive derived state (the filtered list, the preview) inside the Transition, or use useDeferredValue on the value instead.

More examples

Pending state: useTransition instead of the standalone call

The tab buttons stay clickable while the panel renders, and isPending dims the stale panel so the delay reads as intentional rather than as a frozen UI. This is the only difference between the two APIs: the hook form gives you the pending flag, the standalone startTransition does not.

Example · jsx
import { useState, useTransition } from "react";

function Panel({ name }) {
  return <p>Contents of the {name} panel.</p>;
}

export default function App() {
  const [tab, setTab] = useState("posts");
  const [isPending, startTransition] = useTransition();

  function select(next) {
    startTransition(() => setTab(next));
  }

  return (
    <div>
      {["posts", "comments", "settings"].map(name => (
        <button key={name} onClick={() => select(name)} disabled={name === tab}>
          {name}
        </button>
      ))}
      <div style={{ opacity: isPending ? 0.5 : 1 }}>
        <Panel name={tab} />
      </div>
    </div>
  );
}

Async actions and the after-await rule

The output shows "Last saved: Ada" until the simulated request resolves, then the edited name. The inner startTransition is not redundant: React only marks updates scheduled synchronously during the action call, so a set function reached after an await would otherwise be treated as an urgent update. React documents this as a known limitation.

Example · jsx
import { startTransition, useState } from "react";

function saveName(name) {
  // Stands in for a network call; there is no fetch in this reference.
  return new Promise(resolve => setTimeout(() => resolve(name), 300));
}

export default function App() {
  const [name, setName] = useState("Ada");
  const [saved, setSaved] = useState("Ada");

  function handleSave() {
    startTransition(async () => {
      const result = await saveName(name);

      // Everything after `await` has left the Transition scope,
      // so this update needs its own startTransition.
      startTransition(() => setSaved(result));
    });
  }

  return (
    <div>
      <input value={name} onChange={e => setName(e.target.value)} />
      <button onClick={handleSave}>Save</button>
      <p>Last saved: {saved}</p>
    </div>
  );
}

Discussion

  • Be the first to comment on this lesson.