diff --git a/fixtures/view-transition/src/components/App.js b/fixtures/view-transition/src/components/App.js index 5fe8555cf8..028f511107 100644 --- a/fixtures/view-transition/src/components/App.js +++ b/fixtures/view-transition/src/components/App.js @@ -1,6 +1,6 @@ import React, { startTransition, - useInsertionEffect, + useLayoutEffect, useEffect, useState, } from 'react'; @@ -68,7 +68,7 @@ export default function App({assets, initialURL}) { } }, []); const pendingNav = routerState.pendingNav; - useInsertionEffect(() => { + useLayoutEffect(() => { pendingNav(); }, [pendingNav]); return ( diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index aade021fdd..8b17db1dc3 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -1201,8 +1201,9 @@ export function hasInstanceAffectedParent( export function startViewTransition( rootContainer: Container, mutationCallback: () => void, - afterMutationCallback: () => void, layoutCallback: () => void, + afterMutationCallback: () => void, + spawnedWorkCallback: () => void, passiveCallback: () => mixed, ): boolean { const ownerDocument: Document = @@ -1213,11 +1214,15 @@ export function startViewTransition( // $FlowFixMe[prop-missing] const transition = ownerDocument.startViewTransition({ update() { - mutationCallback(); - // TODO: Wait for fonts. + // Note: We read the existence of a pending navigation before we apply the + // mutations. That way we're not waiting on a navigation that we spawned + // from this update. Only navigations that started before this commit. const ownerWindow = ownerDocument.defaultView; const pendingNavigation = ownerWindow.navigation && ownerWindow.navigation.transition; + mutationCallback(); + // TODO: Wait for fonts. + layoutCallback(); if (pendingNavigation) { return pendingNavigation.finished.then( afterMutationCallback, @@ -1241,13 +1246,13 @@ export function startViewTransition( console.error( 'A ViewTransition timed out because a Navigation stalled. ' + 'This can happen if a Navigation is blocked on React itself. ' + - "Such as if it's resolved inside useLayoutEffect. " + - 'This can be solved by moving the resolution to useInsertionEffect.', + "Such as if it's resolved inside useEffect. " + + 'This can be solved by moving the resolution to useLayoutEffect.', ); } }); } - transition.ready.then(layoutCallback, layoutCallback); + transition.ready.then(spawnedWorkCallback, spawnedWorkCallback); transition.finished.then(() => { // $FlowFixMe[prop-missing] ownerDocument.__reactViewTransition = null; diff --git a/packages/react-native-renderer/src/ReactFiberConfigNative.js b/packages/react-native-renderer/src/ReactFiberConfigNative.js index 5687e60637..55699f0973 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigNative.js +++ b/packages/react-native-renderer/src/ReactFiberConfigNative.js @@ -583,8 +583,9 @@ export function hasInstanceAffectedParent( export function startViewTransition( rootContainer: Container, mutationCallback: () => void, - afterMutationCallback: () => void, layoutCallback: () => void, + afterMutationCallback: () => void, + spawnedWorkCallback: () => void, passiveCallback: () => mixed, ): boolean { return false; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 7a387b690b..c79d6d541c 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -637,10 +637,11 @@ const THROTTLED_COMMIT = 2; const NO_PENDING_EFFECTS = 0; const PENDING_MUTATION_PHASE = 1; -const PENDING_AFTER_MUTATION_PHASE = 2; -const PENDING_LAYOUT_PHASE = 3; -const PENDING_PASSIVE_PHASE = 4; -let pendingEffectsStatus: 0 | 1 | 2 | 3 | 4 = 0; +const PENDING_LAYOUT_PHASE = 2; +const PENDING_AFTER_MUTATION_PHASE = 3; +const PENDING_SPAWNED_WORK = 4; +const PENDING_PASSIVE_PHASE = 5; +let pendingEffectsStatus: 0 | 1 | 2 | 3 | 4 | 5 = 0; let pendingEffectsRoot: FiberRoot = (null: any); let pendingFinishedWork: Fiber = (null: any); let pendingEffectsLanes: Lanes = NoLanes; @@ -3432,19 +3433,17 @@ function commitRoot( startViewTransition( root.containerInfo, flushMutationEffects, - flushAfterMutationEffects, flushLayoutEffects, - // TODO: This flushes passive effects at the end of the transition but - // we also schedule work to flush them separately which we really shouldn't. - // We use flushPendingEffects instead of + flushAfterMutationEffects, + flushSpawnedWork, flushPassiveEffects, ); if (!startedViewTransition) { // Flush synchronously. flushMutationEffects(); - // Skip flushAfterMutationEffects - pendingEffectsStatus = PENDING_LAYOUT_PHASE; flushLayoutEffects(); + // Skip flushAfterMutationEffects + flushSpawnedWork(); } } @@ -3457,7 +3456,7 @@ function flushAfterMutationEffects(): void { const finishedWork = pendingFinishedWork; const lanes = pendingEffectsLanes; commitAfterMutationEffects(root, finishedWork, lanes); - pendingEffectsStatus = PENDING_LAYOUT_PHASE; + pendingEffectsStatus = PENDING_SPAWNED_WORK; } function flushMutationEffects(): void { @@ -3503,16 +3502,11 @@ function flushMutationEffects(): void { // componentWillUnmount, but before the layout phase, so that the finished // work is current during componentDidMount/Update. root.current = finishedWork; - pendingEffectsStatus = PENDING_AFTER_MUTATION_PHASE; + pendingEffectsStatus = PENDING_LAYOUT_PHASE; } function flushLayoutEffects(): void { - if ( - pendingEffectsStatus !== PENDING_LAYOUT_PHASE && - // If a startViewTransition times out, we might flush this earlier than - // after mutation phase. In that case, we just skip the after mutation phase. - pendingEffectsStatus !== PENDING_AFTER_MUTATION_PHASE - ) { + if (pendingEffectsStatus !== PENDING_LAYOUT_PHASE) { return; } pendingEffectsStatus = NO_PENDING_EFFECTS; @@ -3520,10 +3514,6 @@ function flushLayoutEffects(): void { const root = pendingEffectsRoot; const finishedWork = pendingFinishedWork; const lanes = pendingEffectsLanes; - const completedRenderEndTime = pendingEffectsRenderEndTime; - const recoverableErrors = pendingRecoverableErrors; - const didIncludeRenderPhaseUpdate = pendingDidIncludeRenderPhaseUpdate; - const suspendedCommitReason = pendingSuspendedCommitReason; const subtreeHasLayoutEffects = (finishedWork.subtreeFlags & LayoutMask) !== NoFlags; @@ -3554,11 +3544,32 @@ function flushLayoutEffects(): void { ReactSharedInternals.T = prevTransition; } } + pendingEffectsStatus = PENDING_AFTER_MUTATION_PHASE; +} + +function flushSpawnedWork(): void { + if ( + pendingEffectsStatus !== PENDING_SPAWNED_WORK && + // If a startViewTransition times out, we might flush this earlier than + // after mutation phase. In that case, we just skip the after mutation phase. + pendingEffectsStatus !== PENDING_AFTER_MUTATION_PHASE + ) { + return; + } + pendingEffectsStatus = NO_PENDING_EFFECTS; // Tell Scheduler to yield at the end of the frame, so the browser has an // opportunity to paint. requestPaint(); + const root = pendingEffectsRoot; + const finishedWork = pendingFinishedWork; + const lanes = pendingEffectsLanes; + const completedRenderEndTime = pendingEffectsRenderEndTime; + const recoverableErrors = pendingRecoverableErrors; + const didIncludeRenderPhaseUpdate = pendingDidIncludeRenderPhaseUpdate; + const suspendedCommitReason = pendingSuspendedCommitReason; + if (enableProfilerTimer && enableComponentPerformanceTrack) { recordCommitEndTime(); logCommitPhase( @@ -3795,6 +3806,8 @@ export function flushPendingEffects(wasDelayedCommit?: boolean): boolean { // Returns whether passive effects were flushed. flushMutationEffects(); flushLayoutEffects(); + // Skip flushAfterMutation if we're forcing this early. + flushSpawnedWork(); return flushPassiveEffects(wasDelayedCommit); } diff --git a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js index 15db19c3c9..c05f59af16 100644 --- a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js +++ b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js @@ -365,8 +365,9 @@ export function hasInstanceAffectedParent( export function startViewTransition( rootContainer: Container, mutationCallback: () => void, - afterMutationCallback: () => void, layoutCallback: () => void, + afterMutationCallback: () => void, + spawnedWorkCallback: () => void, passiveCallback: () => mixed, ): boolean { return false;