Only transitions animate
A plain setState swaps instantly. startTransition lets it animate.
React docsreact.dev
By default,setStateupdates immediately and does not activate<ViewTransition>, only updates wrapped in a Transition,<Suspense>, oruseDeferredValueactivate ViewTransition.
From vercel-react-view-transitions, SKILL.md.
Same toggle, two updates
Press Show details, then hide them.
Plain setStateBad
setOpen(o => !o)startTransitionGood
startTransition(() => setOpen(o => !o))import { startTransition, useState, ViewTransition } from 'react';
function Order() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => startTransition(() => setOpen(o => !o))}>
Details
</button>
{open && (
<ViewTransition enter="slide-up" exit="fade-out">
<Details />
</ViewTransition>
)}
</>
);
}
Deferred values animate too
Type in the filter.
Filter by searchBad
people.filter(match(search))useDeferredValueGood
people.filter(match(deferred))const [search, setSearch] = useState('');
const deferred = useDeferredValue(search);
const visible = people.filter(p => p.name.includes(deferred));
<input value={search} onChange={e => setSearch(e.target.value)} />
{visible.map(p => (
<ViewTransition key={p.id}><Row person={p} /></ViewTransition>
))}
Why it matters, pros and cons
Why it matters
React calls startViewTransition only for Transitions, Suspense reveals and deferred values. A plain update commits synchronously, so there is nothing to animate between.
Pros
- One wrapper, no animation library.
- React starts the browser transition; never call it yourself.
- Unsupported browsers just swap instantly.
Cons
flushSyncskips the animation.- Fast updates batch: A→B→C→D plays as B→D.
- Outside Next.js it needs
react@canary.