What are Lanes in React source code?

Previously we’ve seen how React schedules tasks by their priorities, an example task would be performConcurrentWorkOnRoot(), which targets the fiber tree as a whole, not the level of work on a single fiber.

The concurrent mode is cool because React is able to work on different tasks for each fiber by their priorities, this low level priority is implement with the concept of “Lane”. This might sounds jargon, don’t worry, we’ll dive into it and there are some examples at the end.

1. Three priority systems

Take a look at ensureRootIsScheduled() again, this is one of the entries where scheduling method is called.

js
// We use the highest priority lane to represent the priority of the callback.
const newCallbackPriority = getHighestPriorityLane(nextLanes);
if (newCallbackPriority === SyncLane) {
...
} else {
let schedulerPriorityLevel;
switch (lanesToEventPriority(nextLanes)) {
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediateSchedulerPriority;
break;
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingSchedulerPriority;
break;
case DefaultEventPriority:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
case IdleEventPriority:
schedulerPriorityLevel = IdleSchedulerPriority;
break;
default:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
}
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
js
// We use the highest priority lane to represent the priority of the callback.
const newCallbackPriority = getHighestPriorityLane(nextLanes);
if (newCallbackPriority === SyncLane) {
...
} else {
let schedulerPriorityLevel;
switch (lanesToEventPriority(nextLanes)) {
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediateSchedulerPriority;
break;
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingSchedulerPriority;
break;
case DefaultEventPriority:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
case IdleEventPriority:
schedulerPriorityLevel = IdleSchedulerPriority;
break;
default:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
}
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}

Interesting, from above code we see that the scheduler priority is derived by

  1. getting the highest lane priority - getHighestPriorityLane()
  2. if not SyncLane, map lanes to Event Priority, then map to Scheduler Priority.

Thus we have 3 priority systems

  1. Scheduler priority - used to prioritize tasks in scheduler
  2. Event priority - to mark priority of user event
  3. Lane priority - to mark priority of work

Since they are for different purposes, they are separated but have logic of mapping like above, we’ll cover event system in future episodes, so we don’t touch much details here.

2. What is “Lane” ?

From my youtube video about setState(), we know that a fiber holds a linked list of hooks, and for state hook, it has a update queue which gets run during update(re-render).

Here is the code where an update is created(source)

const lane = requestUpdateLane(fiber);
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
const lane = requestUpdateLane(fiber);
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};

Yep, notice there is field called lane? Lane is to mark the priority of the update, which we can also say mark the priority of a piece of work.

Here are all the lanes in React, just some numbers which is easier to understand in binary forms. Please try to find the 1s.

js
// Lane values below should be kept in sync with getLabelForLane(), used by react-devtools-timeline.
// If those values are changed that package should be rebuilt and redeployed.
export const TotalLanes = 31;
export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;
export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010;
export const InputContinuousLane: Lanes = /* */ 0b0000000000000000000000000000100;
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000001000;
export const DefaultLane: Lanes = /* */ 0b0000000000000000000000000010000;
const TransitionHydrationLane: Lane = /* */ 0b0000000000000000000000000100000;
const TransitionLanes: Lanes = /* */ 0b0000000001111111111111111000000;
const TransitionLane1: Lane = /* */ 0b0000000000000000000000001000000;
const TransitionLane2: Lane = /* */ 0b0000000000000000000000010000000;
const TransitionLane3: Lane = /* */ 0b0000000000000000000000100000000;
const TransitionLane4: Lane = /* */ 0b0000000000000000000001000000000;
const TransitionLane5: Lane = /* */ 0b0000000000000000000010000000000;
const TransitionLane6: Lane = /* */ 0b0000000000000000000100000000000;
const TransitionLane7: Lane = /* */ 0b0000000000000000001000000000000;
const TransitionLane8: Lane = /* */ 0b0000000000000000010000000000000;
const TransitionLane9: Lane = /* */ 0b0000000000000000100000000000000;
const TransitionLane10: Lane = /* */ 0b0000000000000001000000000000000;
const TransitionLane11: Lane = /* */ 0b0000000000000010000000000000000;
const TransitionLane12: Lane = /* */ 0b0000000000000100000000000000000;
const TransitionLane13: Lane = /* */ 0b0000000000001000000000000000000;
const TransitionLane14: Lane = /* */ 0b0000000000010000000000000000000;
const TransitionLane15: Lane = /* */ 0b0000000000100000000000000000000;
const TransitionLane16: Lane = /* */ 0b0000000001000000000000000000000;
const RetryLanes: Lanes = /* */ 0b0000111110000000000000000000000;
const RetryLane1: Lane = /* */ 0b0000000010000000000000000000000;
const RetryLane2: Lane = /* */ 0b0000000100000000000000000000000;
const RetryLane3: Lane = /* */ 0b0000001000000000000000000000000;
const RetryLane4: Lane = /* */ 0b0000010000000000000000000000000;
const RetryLane5: Lane = /* */ 0b0000100000000000000000000000000;
export const SomeRetryLane: Lane = RetryLane1;
export const SelectiveHydrationLane: Lane = /* */ 0b0001000000000000000000000000000;
const NonIdleLanes = /* */ 0b0001111111111111111111111111111;
export const IdleHydrationLane: Lane = /* */ 0b0010000000000000000000000000000;
export const IdleLane: Lanes = /* */ 0b0100000000000000000000000000000;
export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000;
js
// Lane values below should be kept in sync with getLabelForLane(), used by react-devtools-timeline.
// If those values are changed that package should be rebuilt and redeployed.
export const TotalLanes = 31;
export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;
export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010;
export const InputContinuousLane: Lanes = /* */ 0b0000000000000000000000000000100;
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000001000;
export const DefaultLane: Lanes = /* */ 0b0000000000000000000000000010000;
const TransitionHydrationLane: Lane = /* */ 0b0000000000000000000000000100000;
const TransitionLanes: Lanes = /* */ 0b0000000001111111111111111000000;
const TransitionLane1: Lane = /* */ 0b0000000000000000000000001000000;
const TransitionLane2: Lane = /* */ 0b0000000000000000000000010000000;
const TransitionLane3: Lane = /* */ 0b0000000000000000000000100000000;
const TransitionLane4: Lane = /* */ 0b0000000000000000000001000000000;
const TransitionLane5: Lane = /* */ 0b0000000000000000000010000000000;
const TransitionLane6: Lane = /* */ 0b0000000000000000000100000000000;
const TransitionLane7: Lane = /* */ 0b0000000000000000001000000000000;
const TransitionLane8: Lane = /* */ 0b0000000000000000010000000000000;
const TransitionLane9: Lane = /* */ 0b0000000000000000100000000000000;
const TransitionLane10: Lane = /* */ 0b0000000000000001000000000000000;
const TransitionLane11: Lane = /* */ 0b0000000000000010000000000000000;
const TransitionLane12: Lane = /* */ 0b0000000000000100000000000000000;
const TransitionLane13: Lane = /* */ 0b0000000000001000000000000000000;
const TransitionLane14: Lane = /* */ 0b0000000000010000000000000000000;
const TransitionLane15: Lane = /* */ 0b0000000000100000000000000000000;
const TransitionLane16: Lane = /* */ 0b0000000001000000000000000000000;
const RetryLanes: Lanes = /* */ 0b0000111110000000000000000000000;
const RetryLane1: Lane = /* */ 0b0000000010000000000000000000000;
const RetryLane2: Lane = /* */ 0b0000000100000000000000000000000;
const RetryLane3: Lane = /* */ 0b0000001000000000000000000000000;
const RetryLane4: Lane = /* */ 0b0000010000000000000000000000000;
const RetryLane5: Lane = /* */ 0b0000100000000000000000000000000;
export const SomeRetryLane: Lane = RetryLane1;
export const SelectiveHydrationLane: Lane = /* */ 0b0001000000000000000000000000000;
const NonIdleLanes = /* */ 0b0001111111111111111111111111111;
export const IdleHydrationLane: Lane = /* */ 0b0010000000000000000000000000000;
export const IdleLane: Lanes = /* */ 0b0100000000000000000000000000000;
export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000;

Just like lanes on the road, the rule is to drive on different lanes based on your speed, meaning the smaller the lane is the more urgent the work is, the higher priority it is. So SyncLane is 1 here.

There are many lanes. We don’t dive into each of them but rather understand how it works generally in this episode.

2.1 Bitwise operation

The lanes are just numbers, there are a lot of bitwise operations in React source code, let’s get familiar with that.

Here are some examples that explain themselves.

js
export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) {
return (a & b) !== NoLanes;
}
export function isSubsetOfLanes(set: Lanes, subset: Lanes | Lane) {
return (set & subset) === subset;
}
export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
return a | b;
}
export function removeLanes(set: Lanes, subset: Lanes | Lane): Lanes {
return set & ~subset;
}
export function intersectLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
return a & b;
}
js
export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) {
return (a & b) !== NoLanes;
}
export function isSubsetOfLanes(set: Lanes, subset: Lanes | Lane) {
return (set & subset) === subset;
}
export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
return a | b;
}
export function removeLanes(set: Lanes, subset: Lanes | Lane): Lanes {
return set & ~subset;
}
export function intersectLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
return a & b;
}

2.2 remember childLanes?

In the episode of how does React bailout work in reconciliation, we’ve also touched a bit of lanes and childLanes of a fiber. Each fiber knows:

  1. priorities for the work of itself - lanes
  2. priorities for the work of its descendant - childLanes.

3. Look at performConcurrentWorkOnRoot() again

Here is the basic flow of how a work is scheduled and run.

  1. get the nextLanes of the fiber tree
  2. map it to scheduler priority
  3. schedule the task to reconcile
  4. reconciliation happens, process the work from root

I guess the magic lies in actually reconciliation, that is where lane info is used.

js
let lanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes
);
...
prepareFreshStack(root, lanes);
js
let lanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes
);
...
prepareFreshStack(root, lanes);

prepareFreshStack() means restart the reconciliation, remember there is a cursor(workInProgress) to track the current fiber, usually React would pause and resume from previous position, but in case of error or some weird cases, we need to give up current jobs done and redo them from beginining, this is what fresh means.

function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
root.finishedWork = null;
root.finishedLanes = NoLanes;
const timeoutHandle = root.timeoutHandle;
if (timeoutHandle !== noTimeout) {
// The root previous suspended and scheduled a timeout to commit a fallback
// state. Now that we have additional work, cancel the timeout.
root.timeoutHandle = noTimeout;
// $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
cancelTimeout(timeoutHandle);
}
if (workInProgress !== null) {
let interruptedWork = workInProgress.return;
while (interruptedWork !== null) {
unwindInterruptedWork(interruptedWork, workInProgressRootRenderLanes);
interruptedWork = interruptedWork.return;
}
}
workInProgressRoot = root;
workInProgress = createWorkInProgress(root.current, null);
workInProgressRootRenderLanes =
subtreeRenderLanes =
workInProgressRootIncludedLanes =
lanes;
workInProgressRootExitStatus = RootIncomplete;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootInterleavedUpdatedLanes = NoLanes;
workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
enqueueInterleavedUpdates();
}
function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
root.finishedWork = null;
root.finishedLanes = NoLanes;
const timeoutHandle = root.timeoutHandle;
if (timeoutHandle !== noTimeout) {
// The root previous suspended and scheduled a timeout to commit a fallback
// state. Now that we have additional work, cancel the timeout.
root.timeoutHandle = noTimeout;
// $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
cancelTimeout(timeoutHandle);
}
if (workInProgress !== null) {
let interruptedWork = workInProgress.return;
while (interruptedWork !== null) {
unwindInterruptedWork(interruptedWork, workInProgressRootRenderLanes);
interruptedWork = interruptedWork.return;
}
}
workInProgressRoot = root;
workInProgress = createWorkInProgress(root.current, null);
workInProgressRootRenderLanes =
subtreeRenderLanes =
workInProgressRootIncludedLanes =
lanes;
workInProgressRootExitStatus = RootIncomplete;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootInterleavedUpdatedLanes = NoLanes;
workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
enqueueInterleavedUpdates();
}

We can see that in prepareFreshStack() some variables are just reset. There are quite a few of them about lanes.

  1. workInProgressRootRenderLanes
  2. subtreeRenderLanes
  3. workInProgressRootIncludedLanes
  4. workInProgressRootSkippedLanes
  5. workInProgressRootInterleavedUpdatedLanes
  6. workInProgressRootRenderPhaseUpdatedLanes
  7. workInProgressRootPingedLanes

Ok, no clues what they are for now, but workInProgressRootRenderLanes looks simple to dive into.

// The lanes we're rendering
let workInProgressRootRenderLanes: Lanes = NoLanes;
// The lanes we're rendering
let workInProgressRootRenderLanes: Lanes = NoLanes;

As the comment says itself, this is the lanes we’re rendering.

There are a few places it is used, for example here:

export function requestUpdateLane(fiber: Fiber): Lane {
// Special cases
const mode = fiber.mode;
if ((mode & ConcurrentMode) === NoMode) {
return (SyncLane: Lane);
} else if (
!deferRenderPhaseUpdateToNextBatch &&
(executionContext & RenderContext) !== NoContext &&
workInProgressRootRenderLanes !== NoLanes
) {
// This is a render phase update. These are not officially supported. The
// old behavior is to give this the same "thread" (lanes) as
// whatever is currently rendering. So if you call `setState` on a component
// that happens later in the same render, it will flush. Ideally, we want to
// remove the special case and treat them as if they came from an
// interleaved event. Regardless, this pattern is not officially supported.
// This behavior is only a fallback. The flag only exists until we can roll
// out the setState warning, since existing code might accidentally rely on
// the current behavior.
return pickArbitraryLane(workInProgressRootRenderLanes);
}
// Updates originating inside certain React methods, like flushSync, have
// their priority set by tracking it with a context variable.
//
// The opaque type returned by the host config is internally a lane, so we can
// use that directly.
// TODO: Move this type conversion to the event priority module.
const updateLane: Lane = (getCurrentUpdatePriority(): any);
if (updateLane !== NoLane) {
return updateLane;
}
// This update originated outside React. Ask the host environment for an
// appropriate priority, based on the type of event.
//
// The opaque type returned by the host config is internally a lane, so we can
// use that directly.
// TODO: Move this type conversion to the event priority module.
const eventLane: Lane = (getCurrentEventPriority(): any);
return eventLane;
}
export function requestUpdateLane(fiber: Fiber): Lane {
// Special cases
const mode = fiber.mode;
if ((mode & ConcurrentMode) === NoMode) {
return (SyncLane: Lane);
} else if (
!deferRenderPhaseUpdateToNextBatch &&
(executionContext & RenderContext) !== NoContext &&
workInProgressRootRenderLanes !== NoLanes
) {
// This is a render phase update. These are not officially supported. The
// old behavior is to give this the same "thread" (lanes) as
// whatever is currently rendering. So if you call `setState` on a component
// that happens later in the same render, it will flush. Ideally, we want to
// remove the special case and treat them as if they came from an
// interleaved event. Regardless, this pattern is not officially supported.
// This behavior is only a fallback. The flag only exists until we can roll
// out the setState warning, since existing code might accidentally rely on
// the current behavior.
return pickArbitraryLane(workInProgressRootRenderLanes);
}
// Updates originating inside certain React methods, like flushSync, have
// their priority set by tracking it with a context variable.
//
// The opaque type returned by the host config is internally a lane, so we can
// use that directly.
// TODO: Move this type conversion to the event priority module.
const updateLane: Lane = (getCurrentUpdatePriority(): any);
if (updateLane !== NoLane) {
return updateLane;
}
// This update originated outside React. Ask the host environment for an
// appropriate priority, based on the type of event.
//
// The opaque type returned by the host config is internally a lane, so we can
// use that directly.
// TODO: Move this type conversion to the event priority module.
const eventLane: Lane = (getCurrentEventPriority(): any);
return eventLane;
}

Aha, notice it is requestUpdateLane()? It is a bit hard to understand what is going on in above function, but it is clear that current rendering lanes somewhat affect the lanes scheduled while rendering.

Let’s go back to performConcurrentWorkOnRoot().

js
// Determine the next lanes to work on, using the fields stored
// on the root.
let lanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes
);
js
// Determine the next lanes to work on, using the fields stored
// on the root.
let lanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes
);

This decides what lanes would be worked on, getNextLanes() is pretty complex, we’ll skip here, just keep in mind that for the very basic cases getNextLanes() picks up the the lane of highest priority.

js
if (lanes === NoLanes) {
// Defensive coding. This is never expected to happen.
return null;
}
// We disable time-slicing in some cases: if the work has been CPU-bound
// for too long ("expired" work, to prevent starvation), or we're in
// sync-updates-by-default mode.
// TODO: We only check `didTimeout` defensively, to account for a Scheduler
// bug we're still investigating. Once the bug in Scheduler is fixed,
// we can remove this, since we track expiration ourselves.
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);
js
if (lanes === NoLanes) {
// Defensive coding. This is never expected to happen.
return null;
}
// We disable time-slicing in some cases: if the work has been CPU-bound
// for too long ("expired" work, to prevent starvation), or we're in
// sync-updates-by-default mode.
// TODO: We only check `didTimeout` defensively, to account for a Scheduler
// bug we're still investigating. Once the bug in Scheduler is fixed,
// we can remove this, since we track expiration ourselves.
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);

Interesting, we can see that even in concurrent mode, it might fall back to sync mode in some cases. For example, if the lanes includes blocking lanes or some lanes are expired.

Let’s skip the details and move on.

4. updateReducer()

As we have explained before, useState() is mapped to mountState() for initial render and updateState() in the following updates.

The state update happens in updateState().

js
function updateState<S>(
initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
js
function updateState<S>(
initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}

Internally updateReducer() is used. (source)

function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: (I) => S
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
const current: Hook = (currentHook: any);
// The last rebase update that is NOT part of the base state.
let baseQueue = current.baseQueue;
// The last pending update that hasn't been processed yet.
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
// We have new updates that haven't been processed yet.
// We'll add them to the base queue.
if (baseQueue !== null) {
// Merge the pending queue and the base queue.
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
if (baseQueue !== null) {
// We have a queue to process.
const first = baseQueue.next;
let newState = current.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
do {
const updateLane = update.lane;
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
// update/state.
{...}
const clone: Update<S, A> = {
lane: updateLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// Update the remaining priority in the queue.
// TODO: Don't need to accumulate this. Instead, we can remove
// renderLanes from the original lanes.
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane
);
markSkippedUpdateLanes(updateLane);
} else {
// This update does have sufficient priority.
{...}
if (newBaseQueueLast !== null) {
const clone: Update<S, A> = {
// This update is going to be committed so we never want uncommit
// it. Using NoLane works because 0 is a subset of all bitmasks, so
// this will never be skipped by the check above.
lane: NoLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// Process this update.
if (update.hasEagerState) {
// If this update is a state update (not a reducer) and was processed eagerly,
// we can use the eagerly computed state
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = (newBaseQueueFirst: any);
}
// Mark that the fiber performed work, but only if the new state is
// different from the current state.
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
return [hook.memoizedState, dispatch];
}
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: (I) => S
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
const current: Hook = (currentHook: any);
// The last rebase update that is NOT part of the base state.
let baseQueue = current.baseQueue;
// The last pending update that hasn't been processed yet.
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
// We have new updates that haven't been processed yet.
// We'll add them to the base queue.
if (baseQueue !== null) {
// Merge the pending queue and the base queue.
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
if (baseQueue !== null) {
// We have a queue to process.
const first = baseQueue.next;
let newState = current.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
do {
const updateLane = update.lane;
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
// update/state.
{...}
const clone: Update<S, A> = {
lane: updateLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// Update the remaining priority in the queue.
// TODO: Don't need to accumulate this. Instead, we can remove
// renderLanes from the original lanes.
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane
);
markSkippedUpdateLanes(updateLane);
} else {
// This update does have sufficient priority.
{...}
if (newBaseQueueLast !== null) {
const clone: Update<S, A> = {
// This update is going to be committed so we never want uncommit
// it. Using NoLane works because 0 is a subset of all bitmasks, so
// this will never be skipped by the check above.
lane: NoLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// Process this update.
if (update.hasEagerState) {
// If this update is a state update (not a reducer) and was processed eagerly,
// we can use the eagerly computed state
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = (newBaseQueueFirst: any);
}
// Mark that the fiber performed work, but only if the new state is
// different from the current state.
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
return [hook.memoizedState, dispatch];
}

A big one, but let’s just focus on the core part

js
do {
const updateLane = update.lane;
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
// update/state.
...
}
update = update.next;
} while (update !== null && update !== first);
js
do {
const updateLane = update.lane;
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
// update/state.
...
}
update = update.next;
} while (update !== null && update !== first);

Yeah, it loops through the updates and checks the lanes by isSubsetOfLanes, renderLanes is set in renderWithHooks(), tracing back, the root function call is in performUnitOfWork().

js
next = beginWork(current, unitOfWork, subtreeRenderLanes);
js
next = beginWork(current, unitOfWork, subtreeRenderLanes);

Phew, end of talk. This is a lot so far, we’ve seen how the lanes work roughly.

5. Summary

  1. when events happen on fibers, updates are created with Lane info, which is determined by a few factors.
  2. ancestor fibers are marked with childLanes, so for any fiber, we can get the lane info for descendant nodes.
  3. get the highest priority lanes from root → map it to scheduler priority → schedule a task in scheduler to reconcile the fiber tree
  4. in reconciliation, pick the highest priority lanes to work on - current rendering lanes
  5. traverse thought the fiber tree, check the updates on hooks, run the updates that have lanes included in the rendering lanes.

Thus we are able to run separately for multiple updates on single fiber.

6. But what’s the point of Lanes? Let’s look at some example.

A demo explains more than a thousand words.

6.1 Demo - Inputs blocked by rendering long list

Open the first demo, type something in the input, you can feel the lagging, input field is not responding.

It is laggy because we enforced the delay for the rendering of each cell.

function Cell() {
const start = Date.now();
while (Date.now() - start < 1) {}
return <span className={`cell ${COLORS[Math.round(Math.random())]}`} />;
}
function _Cells() {
return (
<div className="cells">
{new Array(1000).fill(0).map((_, index) => (
<Cell key={index} />
))}
</div>
);
}
const Cells = React.memo(_Cells);
function App() {
const [text, setText] = useState("");
return (
<div className="app">
<input
type="text"
value={text}
onChange={(e) => {
setText(e.target.value);
}}
/>
<Cells text={text} />
</div>
);
}
function Cell() {
const start = Date.now();
while (Date.now() - start < 1) {}
return <span className={`cell ${COLORS[Math.round(Math.random())]}`} />;
}
function _Cells() {
return (
<div className="cells">
{new Array(1000).fill(0).map((_, index) => (
<Cell key={index} />
))}
</div>
);
}
const Cells = React.memo(_Cells);
function App() {
const [text, setText] = useState("");
return (
<div className="app">
<input
type="text"
value={text}
onChange={(e) => {
setText(e.target.value);
}}
/>
<Cells text={text} />
</div>
);
}

We can improve the case by separating the lanes for updating <Cells>, by using startTransition(), see the second demo.

6.2 Demo - Input not blocked by moving heavy work to transition lanes

Open the second demo to give it a try

We can see that the input is instantly responding, while the cells are rendered later.

This is because we moved the update of <Cells/> to transition lanes.

function App() {
const [text, setText] = useState("");
const deferredText = React.useDeferredValue(text);
return (
<div className="app">
<input
type="text"
value={text}
onChange={(e) => {
setText(e.target.value);
}}
/>
<Cells text={deferredText} />
</div>
}
function App() {
const [text, setText] = useState("");
const deferredText = React.useDeferredValue(text);
return (
<div className="app">
<input
type="text"
value={text}
onChange={(e) => {
setText(e.target.value);
}}
/>
<Cells text={deferredText} />
</div>
}

The trick here is useDeferredValue(), which puts updates in transition lanes. For details of this built-in API, check out this episode - How does React.useDeferredValue() work internally?

Also open the devtool, you can see the difference of these two.

For the first one:

js
pendingLanes 0000000000000000000000000000001
pendingLanes 0000000000000000000000000000001
performSyncWorkOnRoot()
lanes to work on 0000000000000000000000000000001
workLoopSync
pendingLanes 0000000000000000000000000000000
pendingLanes 0000000000000000000000000000000
js
pendingLanes 0000000000000000000000000000001
pendingLanes 0000000000000000000000000000001
performSyncWorkOnRoot()
lanes to work on 0000000000000000000000000000001
workLoopSync
pendingLanes 0000000000000000000000000000000
pendingLanes 0000000000000000000000000000000

You can see there is only one lane - SyncLane, so the update for the input and the cells are processed in the same batch.

While for the second demo situation is a bit different.

js
pendingLanes 0000000000000000000000000000001
pendingLanes 0000000000000000000000000000001
performSyncWorkOnRoot()
lanes to work on 0000000000000000000000000000001
workLoopSync
pendingLanes 0000000000000000000000001000000
pendingLanes 0000000000000000000000001000000
pendingLanes 0000000000000000000000001000000
performConcurrentWorkOnRoot()
pendingLanes 0000000000000000000000001000000
js
pendingLanes 0000000000000000000000000000001
pendingLanes 0000000000000000000000000000001
performSyncWorkOnRoot()
lanes to work on 0000000000000000000000000000001
workLoopSync
pendingLanes 0000000000000000000000001000000
pendingLanes 0000000000000000000000001000000
pendingLanes 0000000000000000000000001000000
performConcurrentWorkOnRoot()
pendingLanes 0000000000000000000000001000000

We can see there are two passes, first is SyncLane for the input, but for the Cells, it is TransitionLane1.

6.3 Demo - use the internal API to schedule.

My 3rd demo is also easy to understand.

function App() {
const [num, setNum] = React.useState(1);
const renders = React.useRef([]);
renders.current.push(num);
return (
<div>
<button
onClick={() => {
setCurrentUpdatePriority(4);
setNum((num) => num + 1);
setCurrentUpdatePriority(1);
setNum((num) => num * 10);
}}
>
click me
</button>
{renders.current.map((log, i) => (
<p key={i}>{log}</p>
))}
</div>
);
}
function App() {
const [num, setNum] = React.useState(1);
const renders = React.useRef([]);
renders.current.push(num);
return (
<div>
<button
onClick={() => {
setCurrentUpdatePriority(4);
setNum((num) => num + 1);
setCurrentUpdatePriority(1);
setNum((num) => num * 10);
}}
>
click me
</button>
{renders.current.map((log, i) => (
<p key={i}>{log}</p>
))}
</div>
);
}

We called seState() twice at the same time, but each with different update priority (lane), first call is InputContinuousLane, the second one is SyncLane.

So what do you expect for the result ?

If we don’t consider priority, we might think they are process together so 1 -> 20.

Actual result is 1 -> 10 -> 20.

Open devtool and click the button, we would see what is going on

bash
pendingLanes 0000000000000000000000000000100
pendingLanes 0000000000000000000000000000101
pendingLanes 0000000000000000000000000000101
performSyncWorkOnRoot()
lanes to work on 0000000000000000000000000000001
workLoopSync
render App() with state 10
pendingLanes 0000000000000000000000000000100
pendingLanes 0000000000000000000000000000100
performConcurrentWorkOnRoot()
pendingLanes 0000000000000000000000000000100
lanes to work on 0000000000000000000000000000100
shouldTimeSlice false
workLoopSync
render App() with state 20
pendingLanes 0000000000000000000000000000000
pendingLanes 0000000000000000000000000000000
bash
pendingLanes 0000000000000000000000000000100
pendingLanes 0000000000000000000000000000101
pendingLanes 0000000000000000000000000000101
performSyncWorkOnRoot()
lanes to work on 0000000000000000000000000000001
workLoopSync
render App() with state 10
pendingLanes 0000000000000000000000000000100
pendingLanes 0000000000000000000000000000100
performConcurrentWorkOnRoot()
pendingLanes 0000000000000000000000000000100
lanes to work on 0000000000000000000000000000100
shouldTimeSlice false
workLoopSync
render App() with state 20
pendingLanes 0000000000000000000000000000000
pendingLanes 0000000000000000000000000000000

First we processed SyncLane, so 1 * 10 = 10, then process the rest lanes, notice SyncLane hook update still needs to be run for the consistency, so (1 + 1) * 10 = 20.

That’s it for this episode, hope it helps you understand better of React internals.

Want to know more about how React works internally?
Check out my series - React Internals Deep Dive!

😳 Would you like to share my post to more people ?    

❮ Prev: Join our discord server "Hardcore React" to learn React internals together

Next: How Suspense works internally in Concurrent Mode 1 - Reconciling flow