diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 1e80abb8ef8ea..4fd75fe07ecbf 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -144,8 +144,12 @@ if (__DEV__) { export type Hook = { memoizedState: any, + // TODO: These fields are only used by the state and reducer hooks. Consider + // moving them to a separate object. baseState: any, baseUpdate: Update | null, + rebaseEnd: Update | null, + rebaseTime: ExpirationTime, queue: UpdateQueue | null, next: Hook | null, @@ -580,6 +584,8 @@ function mountWorkInProgressHook(): Hook { baseState: null, queue: null, baseUpdate: null, + rebaseEnd: null, + rebaseTime: NoWork, next: null, }; @@ -621,6 +627,8 @@ function updateWorkInProgressHook(): Hook { baseState: currentHook.baseState, queue: currentHook.queue, baseUpdate: currentHook.baseUpdate, + rebaseEnd: currentHook.rebaseEnd, + rebaseTime: currentHook.rebaseTime, next: null, }; @@ -735,8 +743,10 @@ function updateReducer( // The last update in the entire queue const last = queue.last; // The last update that is part of the base state. - const baseUpdate = hook.baseUpdate; const baseState = hook.baseState; + const baseUpdate = hook.baseUpdate; + const rebaseEnd = hook.rebaseEnd; + const rebaseTime = hook.rebaseTime; // Find the first unprocessed update. let first; @@ -755,19 +765,35 @@ function updateReducer( let newState = baseState; let newBaseState = null; let newBaseUpdate = null; + let newRebaseTime = NoWork; + let newRebaseEnd = null; let prevUpdate = baseUpdate; let update = first; - let didSkip = false; + + // Track whether the update is part of a rebase. + // TODO: Should probably split this into two separate loops, instead of + // using a boolean. + let isRebasing = rebaseTime !== NoWork; + do { + if (prevUpdate === rebaseEnd) { + isRebasing = false; + } const updateExpirationTime = update.expirationTime; - if (updateExpirationTime < renderExpirationTime) { + if ( + // Check if this update should be skipped + updateExpirationTime < renderExpirationTime && + // If we're currently rebasing, don't skip this update if we already + // committed it. + (!isRebasing || updateExpirationTime < rebaseTime) + ) { // Priority is insufficient. Skip this update. If this is the first // skipped update, the previous update/state is the new base // update/state. - if (!didSkip) { - didSkip = true; + if (newRebaseTime === NoWork) { newBaseUpdate = prevUpdate; newBaseState = newState; + newRebaseTime = renderExpirationTime; } // Update the remaining priority in the queue. if (updateExpirationTime > remainingExpirationTime) { @@ -787,7 +813,6 @@ function updateReducer( updateExpirationTime, update.suspenseConfig, ); - // Process this update. if (update.eagerReducer === reducer) { // If this update was processed eagerly, and its reducer matches the @@ -802,9 +827,11 @@ function updateReducer( update = update.next; } while (update !== null && update !== first); - if (!didSkip) { - newBaseUpdate = prevUpdate; + if (newRebaseTime === NoWork) { newBaseState = newState; + newBaseUpdate = prevUpdate; + } else { + newRebaseEnd = prevUpdate; } // Mark that the fiber performed work, but only if the new state is @@ -814,8 +841,10 @@ function updateReducer( } hook.memoizedState = newState; - hook.baseUpdate = newBaseUpdate; hook.baseState = newBaseState; + hook.baseUpdate = newBaseUpdate; + hook.rebaseEnd = newRebaseEnd; + hook.rebaseTime = newRebaseTime; queue.lastRenderedState = newState; } diff --git a/packages/react-reconciler/src/ReactUpdateQueue.js b/packages/react-reconciler/src/ReactUpdateQueue.js index 0d2a8f68230bf..8dbe15680187c 100644 --- a/packages/react-reconciler/src/ReactUpdateQueue.js +++ b/packages/react-reconciler/src/ReactUpdateQueue.js @@ -138,6 +138,9 @@ export type UpdateQueue = { firstCapturedEffect: Update | null, lastCapturedEffect: Update | null, + + rebaseTime: ExpirationTime, + rebaseEnd: Update | null, }; export const UpdateState = 0; @@ -172,6 +175,8 @@ export function createUpdateQueue(baseState: State): UpdateQueue { lastEffect: null, firstCapturedEffect: null, lastCapturedEffect: null, + rebaseTime: NoWork, + rebaseEnd: null, }; return queue; } @@ -194,6 +199,9 @@ function cloneUpdateQueue( firstCapturedEffect: null, lastCapturedEffect: null, + + rebaseTime: currentQueue.rebaseTime, + rebaseEnd: currentQueue.rebaseEnd, }; return queue; } @@ -446,15 +454,36 @@ export function processUpdateQueue( let newBaseState = queue.baseState; let newFirstUpdate = null; let newExpirationTime = NoWork; + let newRebaseTime = NoWork; + let newRebaseEnd = null; + let prevUpdate = null; // Iterate through the list of updates to compute the result. let update = queue.firstUpdate; let resultState = newBaseState; + + // Track whether the update is part of a rebase. + // TODO: Should probably split this into two separate loops, instead of + // using a boolean. + const rebaseTime = queue.rebaseTime; + const rebaseEnd = queue.rebaseEnd; + let isRebasing = rebaseTime !== NoWork; + while (update !== null) { const updateExpirationTime = update.expirationTime; - if (updateExpirationTime < renderExpirationTime) { + if (prevUpdate === rebaseEnd) { + isRebasing = false; + } + if ( + // Check if this update should be skipped + updateExpirationTime < renderExpirationTime && + // If we're currently rebasing, don't skip this update if we already + // committed it. + (!isRebasing || updateExpirationTime < rebaseTime) + ) { // This update does not have sufficient priority. Skip it. - if (newFirstUpdate === null) { + if (newRebaseTime === NoWork) { + newRebaseTime = renderExpirationTime; // This is the first skipped update. It will be the first update in // the new list. newFirstUpdate = update; @@ -501,6 +530,7 @@ export function processUpdateQueue( } } // Continue to the next update. + prevUpdate = update; update = update.next; } @@ -508,6 +538,8 @@ export function processUpdateQueue( let newFirstCapturedUpdate = null; update = queue.firstCapturedUpdate; while (update !== null) { + // TODO: Captured updates always have the current render expiration time. + // Shouldn't need this priority check, because they will never be skipped. const updateExpirationTime = update.expirationTime; if (updateExpirationTime < renderExpirationTime) { // This update does not have sufficient priority. Skip it. @@ -565,11 +597,15 @@ export function processUpdateQueue( // We processed every update, without skipping. That means the new base // state is the same as the result state. newBaseState = resultState; + } else { + newRebaseEnd = prevUpdate; } queue.baseState = newBaseState; queue.firstUpdate = newFirstUpdate; queue.firstCapturedUpdate = newFirstCapturedUpdate; + queue.rebaseEnd = newRebaseEnd; + queue.rebaseTime = newRebaseTime; // Set the remaining expiration time to be whatever is remaining in the queue. // This should be fine because the only two other things that contribute to