Transitions
Mark slow updates as non-urgent with useTransition and useDeferredValue, keep old content visible while new content loads, and keep hidden UI alive with Activity.
Not every update is equally important. When you type a letter, you expect to see it now. When you switch to a tab full of books, it's fine if the list takes a moment, as long as the page doesn't freeze while it does.
By default, React treats every state update as urgent: it renders the change straight away and nothing else can happen until that render is done. A Transition tells React that an update is not urgent. React can then prepare it in the background, interrupt it if something more important comes in, and keep showing the current screen until the new one is ready.
useTransition: don't freeze on slow updates
The Books tab below renders 300 deliberately slow items. Click Books and then, right away, Contact. The Contact tab responds immediately, because React abandons the half-finished Books render to deal with your newer click.
Now try removing the Transition. Change selectTab to call setTab(nextTab) directly, then click Books and Contact again. The second click waits until the slow render is finished, so the whole page feels stuck.
useTransition returns two things:
isPendingistruewhile a Transition is waiting to be shown. Use it to give a hint, like the faded tab bar above.startTransition(fn)runsfnstraight away and marks every state update inside it as a Transition.
If you're outside a component, or don't need isPending, you can import a standalone startTransition from react. You used it in the Actions lesson. It works the same way but has no pending flag.
Transitions and Suspense: keep the old screen
In the Suspense lesson, switching books replaced the old book with the fallback. That's jarring when you already had something good on the screen. When a component suspends during a Transition, React keeps showing the content that's already on screen instead of going back to the fallback, and isPending tells you it's working.
The fallback appears only once, on the very first load, when there's nothing to keep. After that, the old book stays visible and fades while the next one loads. This is how routers make page changes feel smooth: React Router and Next.js wrap navigations in Transitions for you.
You wrap setTab('books') in startTransition, and the Books tab suspends while loading. What does the user see during the wait?
useDeferredValue: let a slow part lag behind
Sometimes the slow part isn't something you set yourself. Take a search box that filters a slow list. The input must stay urgent, but the list can be a step behind. useDeferredValue gives you a copy of a value that is allowed to lag:
const deferredQuery = useDeferredValue(query);On each keystroke, React first re-renders with the old deferredQuery, which is fast, so the letter appears at once. Then, in the background, it tries again with the new value. If you type another letter first, it drops that background render and starts over.
Type quickly. The letters appear without delay, and the list fades while it catches up. Two details make this work:
isStalecompares the two values, so you can show the user that the list is out of date.SlowResultsis wrapped inmemo. During the fast render, itsqueryprop hasn't changed, somemolets React skip it. Withoutmemo, React would re-render the slow list in the urgent pass as well, and there'd be nothing gained. In the performance lesson you'll see that the React Compiler adds this kind of skipping for you.
Deep diveuseTransition or useDeferredValue?
They solve the same problem from different sides.
- Use
useTransitionwhen you make the state update, for example in a click handler, and can wrap it. - Use
useDeferredValuewhen you receive a value, such as a prop or an input's state, and want a slow part of the UI to follow it at a distance.
Neither one makes the slow code faster. They make sure the slow code doesn't block the parts that need to respond right away. Unlike tricks such as debouncing, there's no fixed delay: on a fast computer the lag is almost zero.
Activity: hide without forgetting
One more tool belongs to this family. When you hide part of the UI with {isOpen && <Panel />}, React unmounts the panel and its state is gone. That's why a half-written draft disappears when you switch tabs and come back.
<Activity>, added in React 19.2, hides content but keeps its state:
<Activity mode={isVisible ? 'visible' : 'hidden'}>
<Panel />
</Activity>When the mode is 'hidden', React hides the children with CSS (display: none), keeps their state and DOM, and cleans up their Effects. When the mode is 'visible' again, the Effects start again. Type a note in both panels below, switch to Stats, then switch back.
The left panel forgets your note. The right panel keeps it. In today's checkpoint, the Reading List puts the list page inside <Activity>, so your chosen filter is still selected when you come back from a book's detail page.
Activity can also prepare content ahead of time
A hidden Activity still renders its children, at a low priority, when React has nothing more urgent to do. If those children read data with use(), the data starts loading early, so the tab is ready by the time the user opens it. Data fetched in an Effect isn't loaded early, because Effects don't run while hidden.
Make the filter bar non-blocking
This Reading List has 300 books, and each card is slow to render. Clicking a filter freezes the buttons. Make the filter change a Transition so the buttons respond immediately, and fade the list while the new filter is pending.
Show solution
import { useState, useTransition } from 'react';
import { books } from './data.js';
import { BookList } from './BookList.js';
const filters = ['all', 'want', 'reading', 'done'];
export default function App() {
const [filter, setFilter] = useState('all');
const [isPending, startTransition] = useTransition();
const visibleBooks =
filter === 'all' ? books : books.filter((book) => book.status === filter);
function handleFilter(next) {
startTransition(() => {
setFilter(next);
});
}
return (
<>
<div className="filters">
{filters.map((f) => (
<button key={f} aria-pressed={filter === f} onClick={() => handleFilter(f)}>
{f}
</button>
))}
</div>
<div style={{ opacity: isPending ? 0.5 : 1 }}>
<BookList books={visibleBooks} />
</div>
</>
);
}The filter state is still derived the same way. The only change is that setFilter runs inside startTransition, so React can drop a half-finished render when you click another filter. Note that aria-pressed follows filter, which only changes when the Transition finishes. If you want the button to highlight instantly, keep a second, urgent piece of state for the selected button.
You hide a panel with <Activity mode="hidden">. Which statement is true?
Day 6 progress