Understanding React Concurrency
Ankit Kumar
Posted on May 29, 2023
React v18.0 has broken ground by introducing a long-awaited feature: Concurrency! Concurrency refers to having more than one task in progress at once, and concurrent tasks can overlap depending on which is more urgent.
What is concurrency?
It is a way to structure a program by breaking it into pieces that can be executed independently. This is how we can break the limits of using a single thread, and make our application more efficient.
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:
const renderBlocking = (Component) => {
for (let Child of Component) {
renderBlocking(Child);
}
}
Then Concurrent Mode would be implemented like this:
const 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));
}
}
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.
Good bye to (Concurrent) 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.
- startTrnsition
- useTransiiton
- useDefferedValue
Lets now understand all of them with examples
startTransition
The startTransition hook introduced with React 18 helps us keep our app responsive without blocking your user interactions by allowing you to mark specific updates as transitions.
There are two categories of state updates in React:
- Urgent updates: show direct interaction like clicking, typing, etc.
- Transition updates: change UI views
React considers state updates wrapped in startTransition as non-urgent, so they can be suspended or interrupted by urgent updates.
For example, as a user, it would feel more natural to see the letters as you type in a search input field for filtering data, but as expected, the search result may take a while, and that’s OK.
import { startTransition } from 'react';
// Urgent
setInputValue(input);
// Mark any state updates inside as transitions
startTransition(() => {
// Transition
setSearchQuery(input);
})
useTransition
React can also track and update pending state transitions using the useTransition hook with an isPending flag. This lets you display loading feedback to your users, letting them know that work is happening in the background.
The useTransition hook returns two items:
- Boolean flag isPending, which is true 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.
const 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} />
</>
)
}
Conceptually, state updates detect if they are wrapped in a startTransition to decide whether to schedule a blocking or concurrent render.
const startTransition = (stateUpdates) => {
isInsideTransition = true;
stateUpdates();
isInsideTransition = false;
}
const 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
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.
useDefferedValue takes in a state value and a timeout in milliseconds and returns the deferred version of that state value. The timeout tells React how long it should delay the deferred value.
Conceptually, useDeferredValue is a debounce effect and can be implemented as such:
import { useState, useDeferredValue } from "react";
const App = () => {
const [input, setInput] = useState("");
const deferredValue = useDeferredValue(text, { timeoutMs: 3000 });
return (
<div>
<input value={input} onChange={handleChange} />
<MyList text={deferredValue} />
</div>
);
}
With what we have in the code snippet above, the input value will be displayed immediately when a user starts typing, but useDeferredValue will display a previous version of the MyList component for at most 3000 milliseconds.
Concurrent Features & Suspense
The useTransition and useDeferredValue hooks have a purpose beyond opting in concurrent rendering: they also wait for Suspense components to complete.
If you like this post, you can buy me a coffee.
Also, to be notified about my new articles and stories: Follow me on Medium.
Subscribe to my YouTube Channel for educational content on similar topics
Follow me on Hashnode and GitHub, for connecting quickly
You can find me on LinkedIn as it’s a professional network for people like me and you.
Cheers!!!!
Posted on May 29, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.