Chapter 3 of 4
Transitions
Marking updates as non-urgent so typing never stutters.
Some updates must feel instant - a keystroke appearing in an input. Others can wait a moment - the filtered results below it. useTransition lets you say which is which.
import { useState, useTransition } from "react";
function SearchPage() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
function handleChange(event) {
setQuery(event.target.value); // urgent: show the typing
startTransition(() => {
setResults(search(event.target.value)); // can be interrupted
});
}
return (
<>
<input value={query} onChange={handleChange} />
<div style={{ opacity: isPending ? 0.6 : 1 }}>
<Results items={results} />
</div>
</>
);
}React can interrupt and restart a transition. If another keystroke arrives while the results are still rendering, the in-progress work is abandoned and the newer input wins - so the input never lags behind the user.
isPending
The flag lets you show that something is happening without hiding the old content. Dimming or a small spinner beats replacing results with a skeleton the user has already seen.
useDeferredValue
When you cannot wrap the update - because the value comes from a prop, or from a library - defer the value instead. React renders with the old value first, then re-renders with the new one at a lower priority.
function Results({ query }) {
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
const items = useMemo(() => search(deferredQuery), [deferredQuery]);
return (
<div style={{ opacity: isStale ? 0.6 : 1 }}>
<List items={items} />
</div>
);
}Transitions and Suspense together
An update inside startTransition that causes a component to suspend does not show the fallback. React keeps the current screen visible until the new one is ready, which avoids replacing good content with a spinner during navigation.