Enhancing UI Responsiveness in React 18 with useTransition
The useTransition hook landed in React 18 as part of the concurrent rendering features. It lets you mark some state updates as lower priority so the slower work doesn’t block the UI. The payoff is a snappier feel under load.
Typical scenarios may include:
- Navigating between routes: The navigation can be immediate, while the new data loading can happen in the background.
- Search results filtering: The UI updates immediately based on user input, while filtering the results can happen in a deferred way.
Syntax
const [isPending, startTransition] = useTransition()**isPending**: A boolean value that tells you if a transition is in progress. It’s useful for showing loading indicators or skeleton/placeholder UI.**startTransition**: A function that wraps non-urgent updates. React will prioritize any updates outside of this function. You can think of wrapping a state update withstartTransitionas “marking a state as a transition”.
Example
Call useTransition at the top level of your component. You get back the isPending flag and the startTransition function.
import { useState, useTransition } from 'react'
function MyComponent() {
const [isPending, startTransition] = useTransition()
// ...
}Wrap a state update in startTransition to mark it as a transition:
import { useState, useTransition } from 'react'
function MyComponent() {
const [isPending, startTransition] = useTransition()
const [state, setState] = useState()
function handleStateChange(e){
startTransition(() => {
setState(e)
})
}
// ...
}Displaying “Loading” State
Use the isPending flag to render a loading spinner, a skeleton or a placeholder while the transition is in flight.
import { useState, useTransition } from 'react'
function MyComponent() {
const [isPending, startTransition] = useTransition()
// ...
if (isPending) {
return <div>Loading...</div>
}
// ...
}Caveats
**useTransition**is a hook, so it can only be used in function components or custom hooks. If you need to kick off a transition from somewhere else, like a class component or a data library, importstartTransitiondirectly fromreact.**useTransition**only works with state setter functions (e.g., setCount). If you need to defer any other value, like a derived value from a custom hook, reach foruseDeferredValueinstead.- The function(s) you execute in
**startTransition**must all be synchronous for React to mark them as transitions. Asynchronous work, likesetTimeout, promises, or anything awaited, won’t be picked up.
startTransition(async () => {
setTimeout(() => {
setState("something") // ❌
}, 100)
await someAsyncFunction() // ❌
setState("something") // ✅
})- State updates marked as transitions can be interrupted by other state updates. If a higher-priority update lands while a transition is running, React handles the urgent update first and then restarts the transition; it doesn’t resume from where it left off. Re-triggering a transition that’s already in flight also restarts it.
- Don’t use transitions for controlled text inputs. Keystrokes need to land immediately, otherwise the input feels laggy.
Current Limitations (v18.3.1)
- If multiple transitions are in flight at once, React batches them and runs them as one. The team has flagged this as a limitation they plan to address.
- Error Boundary for useTransition is currently only available in React’s canary and experimental channels.