What is "startTransition()"?
What startTransition() is
startTransition(callback)tells React that the state updates insidecallbackcan be deferred (they have low priority).
In simpler terms:
It's a way to tell React: "Do this update, but don't rush, render what matters for UX first (for example, text input)."
Example without startTransition
function Search() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
function handleChange(e) {
const value = e.target.value;
setQuery(value);
// Heavy filtering (blocks input)
const filtered = hugeList.filter(item => item.includes(value));
setResults(filtered);
}
return (
<>
<input value={query} onChange={handleChange} />
<ul>{results.map(r => <li key={r}>{r}</li>)}</ul>
</>
);
}The problem: on fast typing, React has to re-render a huge list for every character. The UI "freezes" - the cursor lags behind.
The fix with startTransition
import { startTransition, useState } from 'react';
function Search() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
function handleChange(e) {
const value = e.target.value;
setQuery(value); // urgent update (text input)
// deferred (low-priority) update
startTransition(() => {
const filtered = hugeList.filter(item => item.includes(value));
setResults(filtered);
});
}
return (
<>
<input value={query} onChange={handleChange} />
<ul>{results.map(r => <li key={r}>{r}</li>)}</ul>
</>
);
}What happens:
- React sees that state updates are inside
startTransition(); - It queues them with low priority;
- It first renders the fast, important changes (
setQuery); - Then, when "free", it does the "heavy" ones (
setResults); - As a result, the interface stays responsive.
Visually
Without startTransition | With startTransition |
|---|---|
| Input is blocked | Input is smooth |
| Rendering a huge list stalls things | React defers rendering the list |
| The UI feels "heavy" | The UI is responsive and alive |
1. How "priority" works in React 18
React now distinguishes two types of updates:
| Type of update | Example | Priority |
|---|---|---|
| Urgent | Text input, clicks, scrolling | Run immediately |
| Non-urgent (Transition) | Updating a list, filtering, loading data | Run when React is "free" |
startTransition() marks updates as "non-urgent".
React can pause, cancel, or restart them
if new urgent updates arrive in the meantime.
2. With useTransition() - managing transition state
React also provides the useTransition() hook -
a convenient wrapper around startTransition() with a loading indicator:
import { useTransition, useState } from 'react';
function Search() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
function handleChange(e) {
const value = e.target.value;
setQuery(value);
startTransition(() => {
const filtered = hugeList.filter(item => item.includes(value));
setResults(filtered);
});
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <p>Updating results...</p>}
<ul>{results.map(r => <li key={r}>{r}</li>)}</ul>
</>
);
}useTransition() returns:
isPending-truewhile the transition is running (handy for showing a loader);startTransition()- a function for running low-priority updates.
3. Asynchronous scenarios: API requests
import { useState, startTransition, Suspense } from "react";
import useSWR from "swr";
const fetcher = (url) => fetch(url).then(r => r.json());
function SearchResults({ query }) {
const { data } = useSWR(`/api/search?q=${query}`, fetcher, { suspense: true });
return <ul>{data.results.map(r => <li key={r}>{r}</li>)}</ul>;
}
export function Search() {
const [query, setQuery] = useState('');
const [displayQuery, setDisplayQuery] = useState('');
function handleChange(e) {
const value = e.target.value;
setQuery(value);
// update the "visible" query with a delay
startTransition(() => setDisplayQuery(value));
}
return (
<>
<input value={query} onChange={handleChange} placeholder="Search..." />
<Suspense fallback={<p>Loading...</p>}>
<SearchResults query={displayQuery} />
</Suspense>
</>
);
}On fast typing:
- React updates the field instantly (
query); - it defers changing
displayQueryand making a new request; - the UI stays responsive even with large amounts of data.
4. Difference from useDeferredValue()
| Hook / API | What it does | Example |
|---|---|---|
startTransition(fn) | defers the execution of state updates | startTransition(() => setList(...)) |
useDeferredValue(value) | defers the use of a value | const deferred = useDeferredValue(value) |
Both solve the same problem - keeping the interface smooth - but they apply at different levels:
startTransition()- when you set state;useDeferredValue()- when you use state in a component.
5. Where startTransition() is useful
| Scenario | Benefit |
|---|---|
| Search across a large array | fast typing, filtering deferred |
| Switching tabs | the old tab stays until the new one is ready |
| Rendering large tables / lists | React doesn't freeze on updates |
| Suspense + API | smooth asynchronous transitions |
| Updating charts / filters | no lag or blocking the UI |
Summary
| What it does | Why it matters |
|---|---|
| Marks updates as "low priority" | React can defer them |
| Keeps the UI responsive | fast actions (typing, clicking) run right away |
| Enables smooth transitions | no "freezes" and "flicker" |
Works with useTransition() and Suspense | full control over async UX |
| Foundation of Concurrent Rendering | React itself decides what to update first |
In short:
startTransition()is a way to tell React: "This update isn't urgent - do it whenever it's convenient."It makes the interface instantly responsive, especially with heavy renders, filtering, and asynchronous loading.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.