Prevent errors from comment node roots with enableViewTransition (#33205)

We have many cases internally where the `containerInstance` resolves to
a comment node. `restoreRootViewTransitionName` is called when
`enableViewTransition` is on, even without introducing a
`<ViewTransition />`. So that means it can crash pages because
`containerInstance.style` is `undefined` just by turning on the flag.

This skips cancel/restore of root view transition name if a comment node is the root.
This commit is contained in:
Jack Pope 2025-05-21 13:57:35 -04:00 committed by GitHub
parent 2388481283
commit 3710c4d4f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 36 additions and 4 deletions

View File

@ -1549,6 +1549,19 @@ export function cancelRootViewTransitionName(rootContainer: Container): void {
rootContainer.nodeType === DOCUMENT_NODE rootContainer.nodeType === DOCUMENT_NODE
? (rootContainer: any).documentElement ? (rootContainer: any).documentElement
: rootContainer.ownerDocument.documentElement; : rootContainer.ownerDocument.documentElement;
if (
!disableCommentsAsDOMContainers &&
rootContainer.nodeType === COMMENT_NODE
) {
if (__DEV__) {
console.warn(
'Cannot cancel root view transition on a comment node. All view transitions will be globally scoped.',
);
}
return;
}
if ( if (
documentElement !== null && documentElement !== null &&
// $FlowFixMe[prop-missing] // $FlowFixMe[prop-missing]
@ -1593,8 +1606,16 @@ export function restoreRootViewTransitionName(rootContainer: Container): void {
// clone the whole document outside of the React too. // clone the whole document outside of the React too.
containerInstance = (rootContainer: any); containerInstance = (rootContainer: any);
} }
// $FlowFixMe[prop-missing] if (
if (containerInstance.style.viewTransitionName === 'root') { !disableCommentsAsDOMContainers &&
containerInstance.nodeType === COMMENT_NODE
) {
return;
}
if (
// $FlowFixMe[prop-missing]
containerInstance.style.viewTransitionName === 'root'
) {
// If we moved the root view transition name to the container in a gesture // If we moved the root view transition name to the container in a gesture
// we need to restore it now. // we need to restore it now.
containerInstance.style.viewTransitionName = ''; containerInstance.style.viewTransitionName = '';
@ -1708,6 +1729,13 @@ export function cloneRootViewTransitionContainer(
containerInstance = (rootContainer: any).body; containerInstance = (rootContainer: any).body;
} else if (rootContainer.nodeName === 'HTML') { } else if (rootContainer.nodeName === 'HTML') {
containerInstance = (rootContainer.ownerDocument.body: any); containerInstance = (rootContainer.ownerDocument.body: any);
} else if (
!disableCommentsAsDOMContainers &&
rootContainer.nodeType === COMMENT_NODE
) {
throw new Error(
'Cannot use a startGestureTransition() with a comment node root.',
);
} else { } else {
// If the container is not the whole document, then we ideally should probably // If the container is not the whole document, then we ideally should probably
// clone the whole document outside of the React too. // clone the whole document outside of the React too.

View File

@ -306,6 +306,7 @@ export let shouldFireAfterActiveInstanceBlur: boolean = false;
let viewTransitionContextChanged: boolean = false; let viewTransitionContextChanged: boolean = false;
let inUpdateViewTransition: boolean = false; let inUpdateViewTransition: boolean = false;
let rootViewTransitionAffected: boolean = false; let rootViewTransitionAffected: boolean = false;
let rootViewTransitionNameCanceled: boolean = false;
function isHydratingParent(current: Fiber, finishedWork: Fiber): boolean { function isHydratingParent(current: Fiber, finishedWork: Fiber): boolean {
if (finishedWork.tag === ActivityComponent) { if (finishedWork.tag === ActivityComponent) {
@ -2737,6 +2738,7 @@ function commitAfterMutationEffectsOnFiber(
switch (finishedWork.tag) { switch (finishedWork.tag) {
case HostRoot: { case HostRoot: {
viewTransitionContextChanged = false; viewTransitionContextChanged = false;
rootViewTransitionNameCanceled = false;
pushViewTransitionCancelableScope(); pushViewTransitionCancelableScope();
recursivelyTraverseAfterMutationEffects(root, finishedWork, lanes); recursivelyTraverseAfterMutationEffects(root, finishedWork, lanes);
if (!viewTransitionContextChanged && !rootViewTransitionAffected) { if (!viewTransitionContextChanged && !rootViewTransitionAffected) {
@ -2755,6 +2757,7 @@ function commitAfterMutationEffectsOnFiber(
} }
// We also cancel the root itself. // We also cancel the root itself.
cancelRootViewTransitionName(root.containerInfo); cancelRootViewTransitionName(root.containerInfo);
rootViewTransitionNameCanceled = true;
} }
popViewTransitionCancelableScope(null); popViewTransitionCancelableScope(null);
break; break;
@ -3613,7 +3616,7 @@ function commitPassiveMountOnFiber(
} }
if (isViewTransitionEligible) { if (isViewTransitionEligible) {
if (supportsMutation) { if (supportsMutation && rootViewTransitionNameCanceled) {
restoreRootViewTransitionName(finishedRoot.containerInfo); restoreRootViewTransitionName(finishedRoot.containerInfo);
} }
} }

View File

@ -544,5 +544,6 @@
"556": "Expected prepareToHydrateHostActivityInstance() to never be called. This error is likely caused by a bug in React. Please file an issue.", "556": "Expected prepareToHydrateHostActivityInstance() to never be called. This error is likely caused by a bug in React. Please file an issue.",
"557": "Expected to have a hydrated activity instance. This error is likely caused by a bug in React. Please file an issue.", "557": "Expected to have a hydrated activity instance. This error is likely caused by a bug in React. Please file an issue.",
"558": "Client rendering an Activity suspended it again. This is a bug in React.", "558": "Client rendering an Activity suspended it again. This is a bug in React.",
"559": "Expected to find a host node. This is a bug in React." "559": "Expected to find a host node. This is a bug in React.",
"560": "Cannot use a startGestureTransition() with a comment node root."
} }