Modern React 13: Transitions for responsive updates
This is part 13 of the Modern React development series.
Some updates are urgent because the user is directly manipulating a control. Other updates can happen in the background because they redraw a large part of the screen. Transitions let React treat those categories differently.
Concept
useTransition marks state updates as non-urgent. React can keep urgent interactions responsive, show pending feedback, and interrupt background rendering work when newer urgent input arrives.
Terms
- Transition: A non-urgent update that React can render in the background.
- Urgent update: An update that should reflect direct input immediately, such as typing into a controlled field.
- Pending state: A boolean that tells the component whether a Transition is still in progress.
- Action: React’s name for the function passed to
startTransition.
Mental model
Think of urgent updates as the steering wheel and Transition updates as the map redraw. The steering wheel responds immediately. The map can catch up without freezing the driver.
How it is used
Use Transitions for tab switches with heavy content, route navigation, filtering large visualizations after input is stored, and Actions that should expose pending state without blocking the rest of the UI.
How to use it
- Keep controlled input state outside the Transition.
- Wrap the expensive or non-urgent state update in
startTransition. - Use
isPendingfor subtle feedback while background rendering finishes. - Combine Transitions with Suspense boundaries to avoid jarring fallback replacement.
- For async Actions, wrap state updates after
awaitin anotherstartTransitionwhen React requires it.
Example: Filter after urgent typing
import { useState, useTransition } from "react";import { ResultGrid } from "./ResultGrid";
export function SearchableGrid({ items }: { items: string[] }) { const [query, setQuery] = useState(""); const [visibleQuery, setVisibleQuery] = useState(""); const [isPending, startTransition] = useTransition();
function handleChange(nextQuery: string) { setQuery(nextQuery); startTransition(() => { setVisibleQuery(nextQuery); }); }
const visible = items.filter((item) => item.includes(visibleQuery));
return ( <> <input value={query} onChange={(event) => handleChange(event.target.value)} /> {isPending && <p>Updating results...</p>} <ResultGrid items={visible} /> </> );}- Trail shoes
- Rain shell
- Camp mug
The input remains urgent. The heavier result update is allowed to lag behind briefly.
Example: Tab switch
import { useState, useTransition } from "react";import { TabButtons, TabPanel } from "./ProjectTabs";
type Tab = "overview" | "activity" | "settings";
export function ProjectTabs() { const [tab, setTab] = useState<Tab>("overview"); const [isPending, startTransition] = useTransition();
function selectTab(nextTab: Tab) { startTransition(() => setTab(nextTab)); }
return ( <> <TabButtons selected={tab} onSelect={selectTab} /> {isPending && <span>Loading tab...</span>} <TabPanel tab={tab} /> </> );}A tab change can be non-urgent when the panel is expensive or can suspend.
Details to watch
- Controlled inputs: Do not transition the update that controls the text the user is typing.
- Immediate call:
startTransitioncalls the passed function immediately and marks synchronous updates inside it. - Async gap: State updates after
awaitmay need a nestedstartTransitionwith current React behavior. - Ordering: Transitions can be interrupted. Design UI so newer intent wins clearly.
Series navigation
- Previous: Part 12: Data loading with Suspense boundaries
- Next: Part 14: Forms with Actions
- Series index: Modern React development