3.1 Why priority exists
Type into a search box that filters ten thousand results, and every keystroke could kick off an enormous re-render. Yet the letters still appear the instant you press them. This lesson answers the question hiding behind that: when one user action causes both a tiny urgent update and a huge slow one, how does React keep the urgent one from waiting behind the slow one?
Some updates can wait, some cannot
Press a key in a search box and two separate things need to happen.
First, the letter you typed must appear in the <input> right away. That instant feedback is the whole point — it is how you know the keystroke registered.
Second, the results list must re-filter to match what you typed. But this can lag a fraction of a second behind. Nobody reads a ten-thousand-row list in the gap between two keystrokes.
Both updates were caused by the same keypress. Yet one is urgent and one is not. A render engine that treats them as equally important is in trouble — and as of Module 2, that is exactly the engine we have.
What goes wrong when every update is equal
Picture a search app: a typing <input> at the top and a list of 10,000 results below it.
Up to now, React does one pass over the whole tree — top to bottom — and then commits once. It has no way to say "render this part now, leave that part for later." Everything in the tree is rendered together or not at all.
So a single keystroke drags React into re-rendering the input and all 10,000 results in the same pass:
WITHOUT Lanes (Scheduler only):
T=0ms User types 'r'
T=1ms Scheduler: "start rendering"
T=2ms React: no lane info → must render EVERYTHING
├─ Render <input> (1 ms)
└─ Render 10,000 results (49 ms) ← BLOCKS UI
T=51ms Commit — user finally sees 'r'
User wanted to type 'e' but had to wait 50 ms!
The input itself renders in about a millisecond. The 10,000 results take roughly 49. Because they share one pass, the commit cannot happen until all of it finishes — so your single letter does not reach the screen until 51 ms in. For 50 ms the page is frozen, and the next letter you wanted to type just sits there.
The trap is subtle: the problem is not that the results are slow. The problem is that React has no way to tell the slow work apart from the urgent work, so it cannot skip ahead.
The fix: stamp each update with a priority
Give every update a label that says how urgent it is. React calls that label a lane. For this lesson, treat a lane simply as a priority tag attached to an update — the next lesson shows how React actually stores it.
In the search app, you tell React that the input update is urgent and the results update can wait:
The real code
function SearchApp() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
function handleInput(e) {
setQuery(e.target.value); // Lane: SyncLane (0b1)
startTransition(() => {
setResults(expensiveFilter(e.target.value)); // Lane: TransitionLane1 (0b1000000)
});
}
return (
<>
<input value={query} onChange={handleInput} />
{results.map(item => <ResultItem key={item} item={item} />)}
</>
);
}
setQuery runs the normal way, so it gets the urgent lane (SyncLane). The setResults call is wrapped in startTransition, which marks it as low priority (a TransitionLane). Same event, two different lanes.
Now React can do the urgent work in its own pass and leave the slow work for a later one. (The 0b1 and 0b1000000 next to each lane are just those tags written as numbers — how the numbers are built is the whole of the next lesson, so don't worry about them yet.)
WITH Lanes:
T=0ms User types 'r'
T=1ms getNextLanes() → 0b1 (SyncLane — highest priority)
T=2ms Render Pass 1 (renderLanes = 0b1):
├─ Render <input> (1 ms)
└─ SKIP all <ResultItem>s (lane check fails)
T=3ms Commit — user sees 'r' and can type next character ✓
T=10ms Render Pass 2 (renderLanes = 0b1000000):
└─ Render 10,000 results (slow but input already updated)
T=500ms Commit results ✓
The keystroke now commits at 3 ms instead of 51. The 10,000 results still render — they are exactly as slow as before — but in a separate pass that runs afterward, without holding your typing hostage.
Priority comes from where you are, not what you change
Here is the part that surprises people. The lane is not decided by the value you pass, and not by which component called setState. It is decided by where the setState runs — React's execution context at that moment.
The same setQuery(value) line gets the urgent lane when it runs inside a click or keydown handler, and a low lane when it runs inside startTransition. The line is identical; the surroundings decide the priority.
The real code
When React has no priority labels to work with, it has no choice but to render the whole tree — there is no lane to check, so nothing can be skipped:
// Hypothetical — no lane info available
function performConcurrentWorkOnRoot(root) {
renderRootConcurrent(root, AllLanes); // must render EVERYTHING
// beginWork(<input>) → render (1 ms)
// beginWork(<ResultItem 0>) → can't skip — no lane to check!
// beginWork(<ResultItem 1>) → render...
// ... 10,000 items, 50 ms of blocking
}
The real engine avoids this by stamping a lane onto every update the moment it is created, inside dispatchSetState. Notice that the priority is read from the current context (requestUpdateLane) and then stored right inside the update object.
Show the full source — where the lane gets stamped on
function dispatchSetState(fiber, queue, action) {
// React checks the current execution context to pick a lane
const lane = requestUpdateLane(fiber);
// e.g.: startTransition() → TransitionLane1
// click / keydown → SyncLane
// scroll / mousemove → InputContinuousLane
// flushSync() → SyncLane
const update = {
lane, // priority stored IN the update object
action,
hasEagerState: false,
eagerState: null,
next: null,
};
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
scheduleUpdateOnFiber(root, fiber, lane);
}
}