React’s scheduler uses priority lanes to decide which updates to process first, and it’s not just about what you click first.
Let’s see it in action. Imagine a complex form with several input fields, and a search bar that triggers an API call.
import React, { useState, useDeferredValue, useMemo } from 'react';
function App() {
const [text, setText] = useState('');
const [list, setList] = useState([]);
// This input is high priority, changes should be immediate
const handleTextChange = (event) => {
setText(event.target.value);
};
// The list rendering is deferred, it can wait
const deferredText = useDeferredValue(text);
// Simulate an expensive list generation
const filteredList = useMemo(() => {
console.log(`Filtering list for: ${deferredText}`);
// In a real app, this would be a complex filter or API call
return Array.from({ length: 1000 }, (_, i) => `Item ${i} matching ${deferredText}`);
}, [deferredText]);
// Simulate fetching data based on deferredText
// In a real app, this would be a network request
React.useEffect(() => {
console.log(`Fetching data for: ${deferredText}`);
// Simulate API call delay
const timeoutId = setTimeout(() => {
setList(filteredList.slice(0, 10)); // Show first 10 results
}, 500); // Simulate network latency
return () => clearTimeout(timeoutId);
}, [deferredText, filteredList]);
return (
<div>
<input type="text" value={text} onChange={handleTextChange} placeholder="Type here..." />
<h2>Results:</h2>
<ul>
{list.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}
export default App;
When you type into the input, the text state updates immediately. However, the expensive filtering and list rendering, which depend on deferredText, are intentionally delayed. This is because useDeferredValue tells React that these updates are less critical and can be batched or interrupted if a more important update (like typing) comes along.
The core problem React’s scheduler solves is how to keep your UI responsive even when there are many updates happening at once, some of which are computationally expensive. Before concurrent rendering, React would process updates one by one in a single thread. If a render took too long, the entire UI would freeze. The scheduler introduces the concept of "priority lanes" – essentially different queues for different types of work.
There are several priority lanes, but the most relevant ones are:
- Immediate Lane: For urgent updates like user input (
onChange,onClick). These need to be processed right away to feel interactive. - User Blocking Lane: For updates triggered by user interaction that aren’t as immediate as typing, but still need to happen before the user gets confused. Think things like showing a modal after a button click.
- Deferred Lane: For updates that can be delayed without a noticeable negative user experience.
useDeferredValueplaces work here. Examples include search results that update as you type or computationally intensive filtering. - Idle Lane: For non-urgent background work that can happen when the browser is idle. This could be prefetching data or pre-rendering parts of the UI that aren’t immediately visible.
When React needs to render, it looks at all the pending work across these lanes. It starts with the highest priority lane and works its way down. If it starts working on a lower-priority task and a higher-priority task becomes available, React can pause the lower-priority work, switch to the higher-priority task, and then resume the lower-priority task later. This is the essence of concurrent rendering.
The useDeferredValue hook is a direct way to leverage the deferred lane. It takes a value and returns a new value that is guaranteed to be at least one render cycle behind the original. React will try to render the component with the original, newer value immediately (if it’s high priority), and then it will schedule a re-render with the deferred value when the CPU is available. This prevents a computationally expensive operation tied to deferredText from blocking the immediate update of the input field.
The useMemo hook, in this context, is crucial. It memoizes the filteredList. If deferredText hasn’t changed since the last render, useMemo will return the cached filteredList without re-computing it, saving precious CPU cycles. This is a performance optimization that works hand-in-hand with the scheduler’s ability to defer work.
What most people miss is that the scheduler doesn’t just defer work; it can interrupt it. If you have a long-running useEffect that’s updating state, and a user clicks a button that triggers an immediate update, React can pause that useEffect in the middle of its execution, render the immediate update, and then resume the useEffect if it’s still relevant. This is managed by the scheduler tracking which "lane" the work belongs to.
The next major concept to grasp is how to manually control priorities with startTransition.