React 18 entered its Beta stage in 2021, and the core APIs of Concurrent Mode gradually stabilized. useTransition is one of the hooks most worth learning ahead of time — it solves a long-standing pain point for React developers: UI jank caused by rendering very large lists.
Problem Background
Suppose you have a search component that filters a list of tens of thousands of items as the user types:
function SearchableList({ items }) {
const [query, setQuery] = useState('')
const [filteredList, setFilteredList] = useState(items)
const handleChange = (e) => {
const value = e.target.value
setQuery(value)
// 同步过滤,万级数据量时明显卡顿
setFilteredList(items.filter(item => item.name.includes(value)))
}
return (
<>
<input value={query} onChange={handleChange} />
<List items={filteredList} />
</>
)
}
With a large dataset, every keystroke triggers an expensive render, and the input visibly stutters.
The useTransition Solution
import { useState, useTransition } from 'react'
function SearchableList({ items }) {
const [query, setQuery] = useState('')
const [filteredList, setFilteredList] = useState(items)
const [isPending, startTransition] = useTransition()
const handleChange = (e) => {
const value = e.target.value
// 高优先级:立即更新输入框
setQuery(value)
// 低优先级:标记为 transition,不阻塞输入
startTransition(() => {
setFilteredList(items.filter(item => item.name.includes(value)))
})
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending ? <Spinner /> : null}
<List items={filteredList} />
</>
)
}
The core idea is to split state updates into two priority levels:
setQueryis an urgent update — it runs immediately so the input stays responsive- The update wrapped in
startTransitionis a transition update — it can be interrupted and won't block user interaction
Difference from Debounce
Many people's first reaction is "isn't this just debounce?" — but the nature is different:
防抖:延迟执行,用户停止输入后才触发过滤
useTransition:立即执行但可中断,保证紧急更新优先
Debounce sacrifices responsiveness, whereas useTransition stays real-time while not blocking interaction.
Using with Suspense
In React 18, useTransition works even better paired with Suspense:
function App() {
const [isPending, startTransition] = useTransition()
const [userId, setUserId] = useState(null)
const handleSelect = (id) => {
startTransition(() => {
setUserId(id)
})
}
return (
<>
<UserList onSelect={handleSelect} />
{isPending && <Skeleton />}
<Suspense fallback={<Loading />}>
<UserProfile userId={userId} />
</Suspense>
</>
)
}
When the user switches quickly between users, the old content stays on screen until the new content loads, avoiding a white-screen flash.
Important Notes
- The update inside a
startTransitioncallback must be synchronous — you can't wrap async operations in it - A transition can't be used to update the value of a controlled input
- Currently
useTransitiononly takes effect in Concurrent Mode; under Legacy Mode it degrades to a synchronous update
Summary
useTransitionis a core API of React 18's Concurrent Mode- It distinguishes urgent updates from transition updates, making rendering interruptible
- It solves jank in scenarios like filtering large lists and switching tabs
- Unlike debounce, it doesn't sacrifice real-time responsiveness
- It's worth starting to learn and experiment with it during the React 18 Beta
