Skip to content

Commit 0d55141

Browse files
committed
Prevent view flattening for direct host children of ViewTransition
Add finalizeViewTransitionChild renderer config function called during completeWork for HostComponents that are direct children of a ViewTransition boundary. Fabric implements it by injecting collapsable: false to prevent native view flattening. DOM is a no-op. Uses a stack cursor (viewTransitionCursor) to track whether the current fiber is inside a ViewTransition — ViewTransition pushes true, HostComponent pushes false (acting as a boundary).
1 parent aff6b07 commit 0d55141

File tree

8 files changed

+107
-6
lines changed

8 files changed

+107
-6
lines changed

packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1498,6 +1498,14 @@ function countClientRects(rects: Array<ClientRect>): number {
14981498
return count;
14991499
}
15001500

1501+
export function finalizeViewTransitionChild(
1502+
type: string,
1503+
props: Props,
1504+
): Props {
1505+
// No-op for DOM. View flattening is a React Native concept.
1506+
return props;
1507+
}
1508+
15011509
export function applyViewTransitionName(
15021510
instance: Instance,
15031511
name: string,

packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,16 @@ export function createViewTransitionInstance(
204204
};
205205
}
206206

207+
export function finalizeViewTransitionChild(
208+
type: string,
209+
props: Props,
210+
): Props {
211+
// Prevent view flattening for direct host children of ViewTransition.
212+
// Without this, Fabric's native-side optimization may remove the view
213+
// from the platform hierarchy, breaking view transition animations.
214+
return Object.assign({}, props, {collapsable: false});
215+
}
216+
207217
export function applyViewTransitionName(
208218
instance: Instance,
209219
name: string,

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ import {shouldError, shouldSuspend} from './ReactFiberReconciler';
188188
import {
189189
pushHostContext,
190190
pushHostContainer,
191+
pushViewTransitionContext,
191192
getRootHostContainer,
192193
} from './ReactFiberHostContext';
193194
import {
@@ -3571,6 +3572,10 @@ function updateViewTransition(
35713572
workInProgress: Fiber,
35723573
renderLanes: Lanes,
35733574
) {
3575+
// Mark direct host children as being inside a ViewTransition so the renderer
3576+
// can finalize them (e.g. prevent view flattening in React Native).
3577+
pushViewTransitionContext(workInProgress);
3578+
35743579
if (workInProgress.stateNode === null) {
35753580
// We previously reset the work-in-progress.
35763581
// We need to create a new ViewTransitionState instance.
@@ -4157,6 +4162,13 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
41574162
}
41584163
// Fallthrough
41594164
}
4165+
case ViewTransitionComponent: {
4166+
if (enableViewTransition) {
4167+
pushViewTransitionContext(workInProgress);
4168+
break;
4169+
}
4170+
// Fallthrough
4171+
}
41604172
}
41614173
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
41624174
}

packages/react-reconciler/src/ReactFiberCompleteWork.js

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,14 @@ import {
127127
mayResourceSuspendCommit,
128128
preloadInstance,
129129
preloadResource,
130+
finalizeViewTransitionChild,
130131
} from './ReactFiberConfig';
131132
import {
132133
getRootHostContainer,
133134
popHostContext,
134135
getHostContext,
136+
getIsInViewTransition,
137+
popViewTransitionContext,
135138
popHostContainer,
136139
} from './ReactFiberHostContext';
137140
import {
@@ -1356,16 +1359,25 @@ function completeWork(
13561359
case HostComponent: {
13571360
popHostContext(workInProgress);
13581361
const type = workInProgress.type;
1362+
1363+
// After popping, check if this HostComponent is a direct host child
1364+
// of a ViewTransition. If so, let the renderer finalize the props
1365+
// (e.g. to prevent view flattening in React Native).
1366+
let instanceProps = newProps;
1367+
if (enableViewTransition && getIsInViewTransition()) {
1368+
instanceProps = finalizeViewTransitionChild(type, instanceProps);
1369+
}
1370+
13591371
if (current !== null && workInProgress.stateNode != null) {
13601372
updateHostComponent(
13611373
current,
13621374
workInProgress,
13631375
type,
1364-
newProps,
1376+
instanceProps,
13651377
renderLanes,
13661378
);
13671379
} else {
1368-
if (!newProps) {
1380+
if (!instanceProps) {
13691381
if (workInProgress.stateNode === null) {
13701382
throw new Error(
13711383
'We must have new props for new mounts. This error is likely ' +
@@ -1397,7 +1409,7 @@ function completeWork(
13971409
finalizeHydratedChildren(
13981410
workInProgress.stateNode,
13991411
type,
1400-
newProps,
1412+
instanceProps,
14011413
currentHostContext,
14021414
)
14031415
) {
@@ -1407,7 +1419,7 @@ function completeWork(
14071419
const rootContainerInstance = getRootHostContainer();
14081420
const instance = createInstance(
14091421
type,
1410-
newProps,
1422+
instanceProps,
14111423
rootContainerInstance,
14121424
currentHostContext,
14131425
workInProgress,
@@ -1425,14 +1437,21 @@ function completeWork(
14251437
finalizeInitialChildren(
14261438
instance,
14271439
type,
1428-
newProps,
1440+
instanceProps,
14291441
currentHostContext,
14301442
)
14311443
) {
14321444
markUpdate(workInProgress);
14331445
}
14341446
}
14351447
}
1448+
1449+
// Ensure memoizedProps reflects the finalized props so that
1450+
// future renders diff against the correct props.
1451+
if (instanceProps !== newProps) {
1452+
workInProgress.memoizedProps = instanceProps;
1453+
}
1454+
14361455
bubbleProperties(workInProgress);
14371456
if (enableViewTransition) {
14381457
// Host Components act as their own View Transitions which doesn't run enter/exit animations.
@@ -2056,6 +2075,7 @@ function completeWork(
20562075
}
20572076
case ViewTransitionComponent: {
20582077
if (enableViewTransition) {
2078+
popViewTransitionContext(workInProgress);
20592079
// We're a component that might need an exit transition. This flag will
20602080
// bubble up to the parent tree to indicate that there's a child that
20612081
// might need an exit View Transition upon unmount.

packages/react-reconciler/src/ReactFiberConfigWithNoViewTransition.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,4 @@ export const stopViewTransition = shim;
4040
export const addViewTransitionFinishedListener = shim;
4141
export type ViewTransitionInstance = null | {name: string, ...};
4242
export const createViewTransitionInstance = shim;
43+
export const finalizeViewTransitionChild = shim;

packages/react-reconciler/src/ReactFiberHostContext.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@ import {
2323
NotPendingTransition,
2424
isPrimaryRenderer,
2525
} from './ReactFiberConfig';
26+
import {enableViewTransition} from 'shared/ReactFeatureFlags';
2627
import {createCursor, push, pop} from './ReactFiberStack';
2728

2829
const contextStackCursor: StackCursor<HostContext | null> = createCursor(null);
30+
31+
const viewTransitionCursor: StackCursor<boolean> = createCursor(false);
2932
const contextFiberStackCursor: StackCursor<Fiber | null> = createCursor(null);
3033
const rootInstanceStackCursor: StackCursor<Container | null> =
3134
createCursor(null);
@@ -93,7 +96,29 @@ function getHostContext(): HostContext {
9396
return context;
9497
}
9598

99+
function pushViewTransitionContext(fiber: Fiber): void {
100+
if (enableViewTransition) {
101+
push(viewTransitionCursor, true, fiber);
102+
}
103+
}
104+
105+
function popViewTransitionContext(fiber: Fiber): void {
106+
if (enableViewTransition) {
107+
pop(viewTransitionCursor, fiber);
108+
}
109+
}
110+
111+
function getIsInViewTransition(): boolean {
112+
return viewTransitionCursor.current;
113+
}
114+
96115
function pushHostContext(fiber: Fiber): void {
116+
// HostComponents act as ViewTransition boundaries. Push false so that
117+
// nested HostComponents below this one are not considered direct VT children.
118+
if (enableViewTransition) {
119+
push(viewTransitionCursor, false, fiber);
120+
}
121+
97122
const stateHook: Hook | null = fiber.memoizedState;
98123
if (stateHook !== null) {
99124
// Propagate the current state to all the descendents.
@@ -129,6 +154,10 @@ function pushHostContext(fiber: Fiber): void {
129154
}
130155

131156
function popHostContext(fiber: Fiber): void {
157+
if (enableViewTransition) {
158+
pop(viewTransitionCursor, fiber);
159+
}
160+
132161
if (contextFiberStackCursor.current === fiber) {
133162
// Do not pop unless this Fiber provided the current context.
134163
// pushHostContext() only pushes Fibers that provide unique contexts.
@@ -159,10 +188,13 @@ function popHostContext(fiber: Fiber): void {
159188

160189
export {
161190
getHostContext,
191+
getIsInViewTransition,
162192
getCurrentRootHostContainer,
163193
getRootHostContainer,
164194
popHostContainer,
165195
popHostContext,
166196
pushHostContainer,
167197
pushHostContext,
198+
pushViewTransitionContext,
199+
popViewTransitionContext,
168200
};

packages/react-reconciler/src/ReactFiberUnwindWork.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,21 @@ import {
3333
LegacyHiddenComponent,
3434
CacheComponent,
3535
TracingMarkerComponent,
36+
ViewTransitionComponent,
3637
} from './ReactWorkTags';
3738
import {DidCapture, NoFlags, ShouldCapture, Update} from './ReactFiberFlags';
3839
import {NoMode, ProfileMode} from './ReactTypeOfMode';
3940
import {
4041
enableProfilerTimer,
4142
enableTransitionTracing,
43+
enableViewTransition,
4244
} from 'shared/ReactFeatureFlags';
4345

44-
import {popHostContainer, popHostContext} from './ReactFiberHostContext';
46+
import {
47+
popHostContainer,
48+
popHostContext,
49+
popViewTransitionContext,
50+
} from './ReactFiberHostContext';
4551
import {
4652
popSuspenseListContext,
4753
popSuspenseHandler,
@@ -243,6 +249,11 @@ function unwindWork(
243249
}
244250
}
245251
return null;
252+
case ViewTransitionComponent:
253+
if (enableViewTransition) {
254+
popViewTransitionContext(workInProgress);
255+
}
256+
return null;
246257
default:
247258
return null;
248259
}
@@ -324,6 +335,11 @@ function unwindInterruptedWork(
324335
}
325336
}
326337
break;
338+
case ViewTransitionComponent:
339+
if (enableViewTransition) {
340+
popViewTransitionContext(interruptedWork);
341+
}
342+
break;
327343
default:
328344
break;
329345
}

packages/react-reconciler/src/forks/ReactFiberConfig.custom.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ export const addViewTransitionFinishedListener =
168168
export const getCurrentGestureOffset = $$$config.getCurrentGestureOffset;
169169
export const createViewTransitionInstance =
170170
$$$config.createViewTransitionInstance;
171+
export const finalizeViewTransitionChild =
172+
$$$config.finalizeViewTransitionChild;
171173
export const clearContainer = $$$config.clearContainer;
172174
export const createFragmentInstance = $$$config.createFragmentInstance;
173175
export const updateFragmentInstanceFiber =

0 commit comments

Comments
 (0)