From 6a473782b8db4784d93ab921d1e363b49b1fb8ab Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 19 Jan 2026 14:22:37 -0500 Subject: [PATCH 1/4] Move font loading to inserted style tag --- .../view-transition/src/components/Page.css | 9 -------- .../view-transition/src/components/Page.js | 21 +++++++++++++++++++ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/fixtures/view-transition/src/components/Page.css b/fixtures/view-transition/src/components/Page.css index 06100a53e8c..63ae718e740 100644 --- a/fixtures/view-transition/src/components/Page.css +++ b/fixtures/view-transition/src/components/Page.css @@ -1,12 +1,3 @@ -.roboto-font { - font-family: "Roboto", serif; - font-optical-sizing: auto; - font-weight: 100; - font-style: normal; - font-variation-settings: - "wdth" 100; -} - .swipe-recognizer { width: 300px; background: #eee; diff --git a/fixtures/view-transition/src/components/Page.js b/fixtures/view-transition/src/components/Page.js index 6227e9ebc5f..f7ad38f4388 100644 --- a/fixtures/view-transition/src/components/Page.js +++ b/fixtures/view-transition/src/components/Page.js @@ -4,6 +4,7 @@ import React, { Activity, useLayoutEffect, useEffect, + useInsertionEffect, useState, useId, useOptimistic, @@ -41,6 +42,26 @@ const b = ( ); function Component() { + // Test inserting fonts with style tags using useInsertionEffect. This is not recommended but + // used to test that gestures etc works with useInsertionEffect so that stylesheet based + // libraries can be properly supported. + useInsertionEffect(() => { + const style = document.createElement('style'); + style.textContent = ` + .roboto-font { + font-family: "Roboto", serif; + font-optical-sizing: auto; + font-weight: 100; + font-style: normal; + font-variation-settings: + "wdth" 100; + } + `; + document.head.appendChild(style); + return () => { + document.head.removeChild(style); + }; + }, []); return ( Date: Mon, 19 Jan 2026 14:50:50 -0500 Subject: [PATCH 2/4] Mount useInsertionEffect in the clone phase This only applies to newly inserted Fibers and not clones (e.g. if they were cloned from a previous offscreen tree which would've inserted them already). --- .../src/ReactFiberApplyGesture.js | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/react-reconciler/src/ReactFiberApplyGesture.js b/packages/react-reconciler/src/ReactFiberApplyGesture.js index f18e33b57ba..9bfaec250e2 100644 --- a/packages/react-reconciler/src/ReactFiberApplyGesture.js +++ b/packages/react-reconciler/src/ReactFiberApplyGesture.js @@ -52,6 +52,14 @@ import { AffectedParentLayout, } from './ReactFiberFlags'; import { + HasEffect as HookHasEffect, + Insertion as HookInsertion, +} from './ReactHookEffectTags'; +import { + FunctionComponent, + ForwardRef, + MemoComponent, + SimpleMemoComponent, HostComponent, HostHoistable, HostSingleton, @@ -72,6 +80,7 @@ import { pushViewTransitionCancelableScope, popViewTransitionCancelableScope, } from './ReactFiberCommitViewTransitions'; +import {commitHookEffectListMount} from './ReactFiberCommitEffects'; import { getViewTransitionName, getViewTransitionClassName, @@ -395,6 +404,28 @@ function recursivelyInsertNewFiber( visitPhase: VisitPhase, ): void { switch (finishedWork.tag) { + case FunctionComponent: + case ForwardRef: + case MemoComponent: + case SimpleMemoComponent: { + recursivelyInsertNew( + finishedWork, + hostParentClone, + parentViewTransition, + visitPhase, + ); + if (finishedWork.flags & Update) { + // Insertion Effects are mounted temporarily during the rendering of the snapshot. + // This does not affect cloned Offscreen content since those would've been mounted + // while inside the offscreen tree already. + // Note that because we are mounting a clone of the DOM tree and the previous DOM + // tree remains mounted during the snapshot, we can't unmount any previous insertion + // effects. This can lead to conflicts but that is similar to what can happen with + // conflicts for two mounted Activity boundaries. + commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork); + } + break; + } case HostHoistable: { if (supportsResources) { // TODO: Hoistables should get optimistically inserted and then removed. From f08b53ab34c10ebbd64209ff69675b6501cb4e2d Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 19 Jan 2026 15:32:22 -0500 Subject: [PATCH 3/4] Unmount useInsertionEffect in the phase that renders the current state This restores things before starting the actual animation whose target snapshot should represent the current state that's mounted. --- .../src/ReactFiberApplyGesture.js | 75 +++++++++++++++---- 1 file changed, 59 insertions(+), 16 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberApplyGesture.js b/packages/react-reconciler/src/ReactFiberApplyGesture.js index 9bfaec250e2..5513f05604d 100644 --- a/packages/react-reconciler/src/ReactFiberApplyGesture.js +++ b/packages/react-reconciler/src/ReactFiberApplyGesture.js @@ -43,6 +43,7 @@ import { } from './ReactFiberMutationTracking'; import { MutationMask, + Placement, Update, ContentReset, NoFlags, @@ -80,7 +81,10 @@ import { pushViewTransitionCancelableScope, popViewTransitionCancelableScope, } from './ReactFiberCommitViewTransitions'; -import {commitHookEffectListMount} from './ReactFiberCommitEffects'; +import { + commitHookEffectListMount, + commitHookEffectListUnmount, +} from './ReactFiberCommitEffects'; import { getViewTransitionName, getViewTransitionClassName, @@ -380,9 +384,10 @@ function recursivelyInsertNew( if ( visitPhase === INSERT_APPEARING_PAIR && parentViewTransition === null && - (parentFiber.subtreeFlags & ViewTransitionNamedStatic) === NoFlags + (parentFiber.subtreeFlags & (ViewTransitionNamedStatic | Placement)) === + NoFlags ) { - // We're just searching for pairs but we have reached the end. + // We're just searching for pairs or insertion effects but we have reached the end. return; } let child = parentFiber.child; @@ -1063,6 +1068,40 @@ function measureExitViewTransitions(placement: Fiber): void { } } +function recursivelyRestoreNew( + finishedWork: Fiber, + nearestMountedAncestor: Fiber, +): void { + // There has to be move a Placement AND an Update flag somewhere below for this + // pass to be relevant since we only apply insertion effects for new components here. + if (((Placement | Update) & finishedWork.subtreeFlags) !== NoFlags) { + let child = finishedWork.child; + while (child !== null) { + recursivelyRestoreNew(child, nearestMountedAncestor); + child = child.sibling; + } + } + switch (finishedWork.tag) { + case FunctionComponent: + case ForwardRef: + case MemoComponent: + case SimpleMemoComponent: { + const current = finishedWork.alternate; + if (current === null && finishedWork.flags & Update) { + // Insertion Effects are mounted temporarily during the rendering of the snapshot. + // We have now already takes a snapshot of the inserted state so we can now unmount + // them to get back into the original state before starting the animation. + commitHookEffectListUnmount( + HookInsertion | HookHasEffect, + finishedWork, + nearestMountedAncestor, + ); + } + break; + } + } +} + function recursivelyApplyViewTransitions(parentFiber: Fiber) { const deletions = parentFiber.deletions; if (deletions !== null) { @@ -1079,7 +1118,13 @@ function recursivelyApplyViewTransitions(parentFiber: Fiber) { // If we have mutations or if this is a newly inserted tree, clone as we go. let child = parentFiber.child; while (child !== null) { - applyViewTransitionsOnFiber(child); + const current = child.alternate; + if (current === null) { + measureExitViewTransitions(child); + recursivelyRestoreNew(child, parentFiber); + } else { + applyViewTransitionsOnFiber(child, current); + } child = child.sibling; } } else { @@ -1090,13 +1135,7 @@ function recursivelyApplyViewTransitions(parentFiber: Fiber) { } } -function applyViewTransitionsOnFiber(finishedWork: Fiber) { - const current = finishedWork.alternate; - if (current === null) { - measureExitViewTransitions(finishedWork); - return; - } - +function applyViewTransitionsOnFiber(finishedWork: Fiber, current: Fiber) { const flags = finishedWork.flags; // The effect flag should be checked *after* we refine the type of fiber, // because the fiber tag is more specific. An exception is any flag related @@ -1107,12 +1146,16 @@ function applyViewTransitionsOnFiber(finishedWork: Fiber) { break; } case OffscreenComponent: { - if (flags & Visibility) { - const newState: OffscreenState | null = finishedWork.memoizedState; - const isHidden = newState !== null; - if (!isHidden) { + const newState: OffscreenState | null = finishedWork.memoizedState; + const isHidden = newState !== null; + const wasHidden = current.memoizedState !== null; + if (!isHidden) { + if (wasHidden) { measureExitViewTransitions(finishedWork); - } else if (current !== null && current.memoizedState === null) { + } + recursivelyRestoreNew(finishedWork, finishedWork); + } else { + if (!wasHidden) { // Was previously mounted as visible but is now hidden. commitEnterViewTransitions(current, true); } From ffaae720e8838b23f53a6aa74abeab35964020e3 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 19 Jan 2026 17:58:20 -0500 Subject: [PATCH 4/4] Apply view transitions in offscreen trees that didn't change state This is just like an update. --- packages/react-reconciler/src/ReactFiberApplyGesture.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberApplyGesture.js b/packages/react-reconciler/src/ReactFiberApplyGesture.js index 5513f05604d..08637e4d8d1 100644 --- a/packages/react-reconciler/src/ReactFiberApplyGesture.js +++ b/packages/react-reconciler/src/ReactFiberApplyGesture.js @@ -1136,7 +1136,6 @@ function recursivelyApplyViewTransitions(parentFiber: Fiber) { } function applyViewTransitionsOnFiber(finishedWork: Fiber, current: Fiber) { - const flags = finishedWork.flags; // The effect flag should be checked *after* we refine the type of fiber, // because the fiber tag is more specific. An exception is any flag related // to reconciliation, because those can be set on all fiber types. @@ -1152,8 +1151,10 @@ function applyViewTransitionsOnFiber(finishedWork: Fiber, current: Fiber) { if (!isHidden) { if (wasHidden) { measureExitViewTransitions(finishedWork); + recursivelyRestoreNew(finishedWork, finishedWork); + } else { + recursivelyApplyViewTransitions(finishedWork); } - recursivelyRestoreNew(finishedWork, finishedWork); } else { if (!wasHidden) { // Was previously mounted as visible but is now hidden.