Understanding React Concurrency
React v18.0 has broken ground by introducing a long-awaited feature: Concurrency!
Unfortunately, despite a deluge of resources explaining how to use it, explanations of how it works are sparse.
As it is a low-level feature, it’s not critical to understand React’s idea of concurrency, but it doesn’t hurt!
This post does not attempt to exhaustively document React’s Concurrent API and best practices. It is best read alongside React’s documentation pages which are linked throughout.
What is React Concurrency?⌗
You've Got Mail!
No more than five emails per year, all densely packed with knowledge.
The basic premise of React concurrency is to re-work the rendering process such that while rendering the next view, the current view is kept responsive.
Concurrent Mode was a proposal the React team had to improve application performance. The idea was to break up the rendering process into interruptible units of work.
Under the hood, this would be implemented by wrapping component renders in a requestIdleCallback()
call, keeping applications responsive during the rendering process.
So hypothetically, if Blocking Mode were implemented like this:
function renderBlocking(Component) {
for (let Child of Component) {
renderBlocking(Child);
}
}
Then Concurrent Mode would be implemented like this:
function renderConcurrent(Component) {
// Interrupt rendering process if state out-dated
if (isCancelled) return;
for (let Child of Component) {
// Wait until browser isn't busy (no inputs to process)
requestIdleCallback(() => renderConcurrent(Child));
}
}
Read the Practical Guide To Not Blocking The Event Loop to understand why this keeps the application responsive!
If you’re curious how React does this in reality, take a peek at the implementation of React’s scheduler
package. After initially using requestIdleCallback
, React switched to requestAnimationFrame
, and later to a user-space timer.
No Mode, Only Features⌗
The Concurrent Mode plan did not materialize for backward-compatibility reasons.
Instead, the React team pivoted to Concurrent Features, a set of new APIs selectively enabling concurrent rendering. So far, React has introduced two new hooks to opt into a concurrent render.
useTransition
⌗
The useTransition
hook returns two items:
- Boolean flag
isPending
, which istrue
if a concurrent render is in progress - Function
startTransition
, which dispatches a new concurrent render
To use it, wrap setState
calls in a startTransition
callback.
function MyCounter() {
const [isPending, startTransition] = useTransition();
const [count, setCount] = useState(0);
const increment = useCallback(() => {
startTransition(() => {
// Run this update concurrently
setCount(count => count + 1);
});
}, []);
return (
<>
<button onClick={increment}>Count {count}</button>
<span>{isPending ? "Pending" : "Not Pending"}</span>
// Component which benefits from concurrency
<ManySlowComponents count={count} />
</>
)
}
Try it out:
Conceptually, state updates detect if they are wrapped in a startTransition
to decide whether to schedule a blocking or concurrent render.
function startTransition(stateUpdates) {
isInsideTransition = true;
stateUpdates();
isInsideTransition = false;
}
function setState() {
if (isInsideTransition) {
// Schedule concurrent render
} else {
// Schedule blocking render
}
}
An important caveat of useTransition
is that it cannot be used for controlled inputs. For those cases, it is best to use useDeferredValue
.
useDeferredValue
⌗
You've Got Mail!
No more than five emails per year, all densely packed with knowledge.
The useDeferredValue
hook is a convenient hook for cases where you do not have the opportunity to wrap the state update in startTransition
but still wish to run the update concurrently.
An example of where this occurs is child components receiving a new value from the parent.
Conceptually, useDeferredValue
is a debounce effect and can be implemented as such:
function useDeferredValue<T>(value: T) {
const [isPending, startTransition] = useTransition();
const [state, setState] = useState(value);
useEffect(() => {
// Dispatch concurrent render
// when input changes
startTransition(() => {
setState(value);
});
}, [value])
return state;
}
It is used the same way as an input debounce hook:
function Child({ value }) {
const deferredValue = useDeferredValue(value);
// ...
}
Concurrent Features & Suspense⌗
The useTransition
and useDeferredValue
hooks have a purpose beyond opting in concurrent rendering: they also wait for Suspense components to complete.
A future post will cover the topic of Suspense and their roles in it. Subscribe to the newsletter to be the first to read it.
In the meantime, you can learn the basic principles of Suspense by reading a newer BBSS post: Learn Suspense by Building a Suspense-Enabled Library.