diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.new.js b/packages/react-reconciler/src/ReactFiberClassComponent.new.js index 9ed6dc23c9f16..389b4877852db 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.new.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.new.js @@ -40,6 +40,7 @@ import { import { enqueueUpdate, + entangleTransitions, processUpdateQueue, checkHasForceUpdateAfterProcessing, resetHasForceUpdateBeforeProcessing, @@ -214,7 +215,10 @@ const classComponentUpdater = { } enqueueUpdate(fiber, update, lane); - scheduleUpdateOnFiber(fiber, lane, eventTime); + const root = scheduleUpdateOnFiber(fiber, lane, eventTime); + if (root !== null) { + entangleTransitions(root, fiber, lane); + } if (__DEV__) { if (enableDebugTracing) { @@ -246,7 +250,10 @@ const classComponentUpdater = { } enqueueUpdate(fiber, update, lane); - scheduleUpdateOnFiber(fiber, lane, eventTime); + const root = scheduleUpdateOnFiber(fiber, lane, eventTime); + if (root !== null) { + entangleTransitions(root, fiber, lane); + } if (__DEV__) { if (enableDebugTracing) { @@ -277,7 +284,10 @@ const classComponentUpdater = { } enqueueUpdate(fiber, update, lane); - scheduleUpdateOnFiber(fiber, lane, eventTime); + const root = scheduleUpdateOnFiber(fiber, lane, eventTime); + if (root !== null) { + entangleTransitions(root, fiber, lane); + } if (__DEV__) { if (enableDebugTracing) { diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 374d383030cdb..69f75d47925f3 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -45,6 +45,8 @@ import { isSubsetOfLanes, mergeLanes, removeLanes, + intersectLanes, + isTransitionLane, markRootEntangled, markRootMutableRead, getCurrentUpdateLanePriority, @@ -104,7 +106,11 @@ import {getIsRendering} from './ReactCurrentFiber'; import {logStateUpdateScheduled} from './DebugTracing'; import {markStateUpdateScheduled} from './SchedulingProfiler'; import {CacheContext} from './ReactFiberCacheComponent.new'; -import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.new'; +import { + createUpdate, + enqueueUpdate, + entangleTransitions, +} from './ReactUpdateQueue.new'; import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new'; const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; @@ -121,6 +127,7 @@ type Update = {| export type UpdateQueue = {| pending: Update | null, interleaved: Update | null, + lanes: Lanes, dispatch: (A => mixed) | null, lastRenderedReducer: ((S, A) => S) | null, lastRenderedState: S | null, @@ -654,6 +661,7 @@ function mountReducer( const queue = (hook.queue = { pending: null, interleaved: null, + lanes: NoLanes, dispatch: null, lastRenderedReducer: reducer, lastRenderedState: (initialState: any), @@ -811,6 +819,10 @@ function updateReducer( markSkippedUpdateLanes(interleavedLane); interleaved = ((interleaved: any).next: Update); } while (interleaved !== lastInterleaved); + } else if (baseQueue === null) { + // `queue.lanes` is used for entangling transitions. We can set it back to + // zero once the queue is empty. + queue.lanes = NoLanes; } const dispatch: Dispatch = (queue.dispatch: any); @@ -1102,6 +1114,7 @@ function useMutableSource( const newQueue = { pending: null, interleaved: null, + lanes: NoLanes, dispatch: null, lastRenderedReducer: basicStateReducer, lastRenderedState: snapshot, @@ -1158,6 +1171,7 @@ function mountState( const queue = (hook.queue = { pending: null, interleaved: null, + lanes: NoLanes, dispatch: null, lastRenderedReducer: basicStateReducer, lastRenderedState: (initialState: any), @@ -1821,6 +1835,9 @@ function refreshCache(fiber: Fiber, seedKey: ?() => T, seedValue: T) { const lane = requestUpdateLane(provider); const eventTime = requestEventTime(); const root = scheduleUpdateOnFiber(provider, lane, eventTime); + if (root !== null) { + entangleTransitions(root, fiber, lane); + } const seededCache = new Map(); if (seedKey !== null && seedKey !== undefined && root !== null) { @@ -1960,7 +1977,22 @@ function dispatchAction( warnIfNotCurrentlyActingUpdatesInDev(fiber); } } - scheduleUpdateOnFiber(fiber, lane, eventTime); + const root = scheduleUpdateOnFiber(fiber, lane, eventTime); + + if (isTransitionLane(lane) && root !== null) { + let queueLanes = queue.lanes; + + // If any entangled lanes are no longer pending on the root, then they + // must have finished. We can remove them from the + queueLanes = intersectLanes(queueLanes, root.pendingLanes); + + // Entangle the new transition lane with the other transition lanes. + const newQueueLanes = mergeLanes(queueLanes, lane); + if (newQueueLanes !== queueLanes) { + queue.lanes = newQueueLanes; + markRootEntangled(root, newQueueLanes); + } + } } if (__DEV__) { diff --git a/packages/react-reconciler/src/ReactFiberLane.new.js b/packages/react-reconciler/src/ReactFiberLane.new.js index 4570c10a62b42..88c4d1946cb6d 100644 --- a/packages/react-reconciler/src/ReactFiberLane.new.js +++ b/packages/react-reconciler/src/ReactFiberLane.new.js @@ -310,9 +310,8 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes { } if (enableTransitionEntanglement) { - // We don't need to include higher priority lanes, because in this - // experiment we always unsuspend all transitions whenever we receive - // an update. + // We don't need to do anything extra here, because we apply per-lane + // transition entanglement in the entanglement loop below. } else { // If there are higher priority lanes, we'll include them even if they // are suspended. @@ -492,6 +491,10 @@ export function includesOnlyTransitions(lanes: Lanes) { return (lanes & TransitionLanes) === lanes; } +export function isTransitionLane(lane: Lane) { + return (lane & TransitionLanes) !== 0; +} + // To ensure consistency across multiple updates in the same event, this should // be a pure function, so that it always returns the same lane for given inputs. export function findUpdateLane( @@ -634,6 +637,10 @@ 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; +} + // Seems redundant, but it changes the type from a single lane (used for // updates) to a group of lanes (used for flushing work). export function laneToLanes(lane: Lane): Lanes { diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js index 08ec3c8eac87e..70923424e8870 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.new.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js @@ -65,7 +65,11 @@ import { IsThisRendererActing, act, } from './ReactFiberWorkLoop.new'; -import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.new'; +import { + createUpdate, + enqueueUpdate, + entangleTransitions, +} from './ReactUpdateQueue.new'; import { isRendering as ReactCurrentFiberIsRendering, current as ReactCurrentFiberCurrent, @@ -315,7 +319,10 @@ export function updateContainer( } enqueueUpdate(current, update, lane); - scheduleUpdateOnFiber(current, lane, eventTime); + const root = scheduleUpdateOnFiber(current, lane, eventTime); + if (root !== null) { + entangleTransitions(root, current, lane); + } return lane; } diff --git a/packages/react-reconciler/src/ReactUpdateQueue.new.js b/packages/react-reconciler/src/ReactUpdateQueue.new.js index 84575950c3465..d0bf5d1ea2ccd 100644 --- a/packages/react-reconciler/src/ReactUpdateQueue.new.js +++ b/packages/react-reconciler/src/ReactUpdateQueue.new.js @@ -84,7 +84,7 @@ // regardless of priority. Intermediate state may vary according to system // resources, but the final state is always the same. -import type {Fiber} from './ReactInternalTypes'; +import type {Fiber, FiberRoot} from './ReactInternalTypes'; import type {Lanes, Lane} from './ReactFiberLane.new'; import { @@ -92,6 +92,9 @@ import { NoLanes, isSubsetOfLanes, mergeLanes, + isTransitionLane, + intersectLanes, + markRootEntangled, } from './ReactFiberLane.new'; import { enterDisallowedContextReadInDEV, @@ -128,6 +131,7 @@ export type Update = {| export type SharedQueue = {| pending: Update | null, interleaved: Update | null, + lanes: Lanes, |}; export type UpdateQueue = {| @@ -167,6 +171,7 @@ export function initializeUpdateQueue(fiber: Fiber): void { shared: { pending: null, interleaved: null, + lanes: NoLanes, }, effects: null, }; @@ -260,6 +265,30 @@ export function enqueueUpdate( } } +export function entangleTransitions(root: FiberRoot, fiber: Fiber, lane: Lane) { + const updateQueue = fiber.updateQueue; + if (updateQueue === null) { + // Only occurs if the fiber has been unmounted. + return; + } + + const sharedQueue: SharedQueue = (updateQueue: any).shared; + if (isTransitionLane(lane) && root !== null) { + let queueLanes = sharedQueue.lanes; + + // If any entangled lanes are no longer pending on the root, then they + // must have finished. We can remove them from the + queueLanes = intersectLanes(queueLanes, root.pendingLanes); + + // Entangle the new transition lane with the other transition lanes. + const newQueueLanes = mergeLanes(queueLanes, lane); + if (newQueueLanes !== queueLanes) { + sharedQueue.lanes = newQueueLanes; + markRootEntangled(root, newQueueLanes); + } + } +} + export function enqueueCapturedUpdate( workInProgress: Fiber, capturedUpdate: Update, @@ -595,6 +624,10 @@ export function processUpdateQueue( newLanes = mergeLanes(newLanes, interleaved.lane); interleaved = ((interleaved: any).next: Update); } while (interleaved !== lastInterleaved); + } else if (firstBaseUpdate === null) { + // `queue.lanes` is used for entangling transitions. We can set it back to + // zero once the queue is empty. + queue.shared.lanes = NoLanes; } // Set the remaining expiration time to be whatever is remaining in the queue.