diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts
index bb0d662c4f67e..6ab9ee79c7412 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts
@@ -992,9 +992,11 @@ function returnsNonNode(
}
}
},
+ // Skip traversing all nested functions and their return statements
ArrowFunctionExpression: skipNestedFunctions(node),
FunctionExpression: skipNestedFunctions(node),
FunctionDeclaration: skipNestedFunctions(node),
+ ObjectMethod: node => node.skip(),
});
return !hasReturn || returnsNonNode;
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-nested-object-method.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-nested-object-method.expect.md
new file mode 100644
index 0000000000000..4c89f59919f0f
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-nested-object-method.expect.md
@@ -0,0 +1,63 @@
+
+## Input
+
+```javascript
+// @compilationMode(infer)
+
+import {Stringify} from 'shared-runtime';
+
+function Test() {
+ const context = {
+ testFn() {
+ // if it is an arrow function its work
+ return () => 'test'; // it will break compile if returns an arrow fn
+ },
+ };
+
+ return ;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Test,
+ params: [{}],
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime"; // @compilationMode(infer)
+
+import { Stringify } from "shared-runtime";
+
+function Test() {
+ const $ = _c(1);
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ const context = {
+ testFn() {
+ return _temp;
+ },
+ };
+
+ t0 = ;
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ return t0;
+}
+function _temp() {
+ return "test";
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Test,
+ params: [{}],
+};
+
+```
+
+### Eval output
+(kind: ok)
{"value":{"testFn":{"kind":"Function","result":{"kind":"Function","result":"test"}}},"shouldInvokeFns":true}
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-nested-object-method.jsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-nested-object-method.jsx
new file mode 100644
index 0000000000000..c8e396b057f8f
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-nested-object-method.jsx
@@ -0,0 +1,19 @@
+// @compilationMode(infer)
+
+import {Stringify} from 'shared-runtime';
+
+function Test() {
+ const context = {
+ testFn() {
+ // if it is an arrow function its work
+ return () => 'test'; // it will break compile if returns an arrow fn
+ },
+ };
+
+ return ;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Test,
+ params: [{}],
+};
diff --git a/fixtures/view-transition/src/components/Page.js b/fixtures/view-transition/src/components/Page.js
index 8204643a3ec2f..688688bcd6c65 100644
--- a/fixtures/view-transition/src/components/Page.js
+++ b/fixtures/view-transition/src/components/Page.js
@@ -57,7 +57,7 @@ export default function Page({url, navigate}) {
}}>
{show ? 'A' : 'B'}
-
+
{show ? (
@@ -92,7 +92,7 @@ export default function Page({url, navigate}) {
!!
- {show ?
:
}
+ {show ?
: null}
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js
index 8e98b763b7afb..e57f54e6687e6 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.js
@@ -203,7 +203,10 @@ import {
OffscreenDetached,
OffscreenPassiveEffectsConnected,
} from './ReactFiberActivityComponent';
-import {getViewTransitionName} from './ReactFiberViewTransitionComponent';
+import {
+ getViewTransitionName,
+ getViewTransitionClassName,
+} from './ReactFiberViewTransitionComponent';
import {
TransitionRoot,
TransitionTracingMarker,
@@ -504,7 +507,7 @@ function commitBeforeMutationEffectsOnFiber(
// We should just stash the parent ViewTransitionComponent and continue
// walking the tree until we find HostComponent but to do that we need
// to use a stack which requires refactoring this phase.
- commitBeforeUpdateViewTransition(current);
+ commitBeforeUpdateViewTransition(current, finishedWork);
}
}
break;
@@ -663,24 +666,31 @@ function commitAppearingPairViewTransitions(placement: Fiber): void {
'Found a pair with an auto name. This is a bug in React.',
);
}
- // We found a new appearing view transition with the same name as this deletion.
- // We'll transition between them.
- viewTransitionHostInstanceIdx = 0;
- const inViewport = applyViewTransitionToHostInstances(
- child.child,
- props.name,
+ const name = props.name;
+ const className: ?string = getViewTransitionClassName(
props.className,
- null,
- false,
+ props.share,
);
- if (!inViewport) {
- // This boundary is exiting within the viewport but is going to leave the viewport.
- // Instead, we treat this as an exit of the previous entry by reverting the new name.
- // Ideally we could undo the old transition but it's now too late. It's also on its
- // on snapshot. We have know was for it to paint onto the original group.
- // TODO: This will lead to things unexpectedly having exit animations that normally
- // wouldn't happen. Consider if we should just let this fly off the screen instead.
- restoreViewTransitionOnHostInstances(child.child, false);
+ if (className !== 'none') {
+ // We found a new appearing view transition with the same name as this deletion.
+ // We'll transition between them.
+ viewTransitionHostInstanceIdx = 0;
+ const inViewport = applyViewTransitionToHostInstances(
+ child.child,
+ name,
+ className,
+ null,
+ false,
+ );
+ if (!inViewport) {
+ // This boundary is exiting within the viewport but is going to leave the viewport.
+ // Instead, we treat this as an exit of the previous entry by reverting the new name.
+ // Ideally we could undo the old transition but it's now too late. It's also on its
+ // on snapshot. We have know was for it to paint onto the original group.
+ // TODO: This will lead to things unexpectedly having exit animations that normally
+ // wouldn't happen. Consider if we should just let this fly off the screen instead.
+ restoreViewTransitionOnHostInstances(child.child, false);
+ }
}
}
}
@@ -691,29 +701,37 @@ function commitAppearingPairViewTransitions(placement: Fiber): void {
function commitEnterViewTransitions(placement: Fiber): void {
if (placement.tag === ViewTransitionComponent) {
+ const state: ViewTransitionState = placement.stateNode;
const props: ViewTransitionProps = placement.memoizedProps;
- const name = getViewTransitionName(props, placement.stateNode);
- viewTransitionHostInstanceIdx = 0;
- const inViewport = applyViewTransitionToHostInstances(
- placement.child,
- name,
+ const name = getViewTransitionName(props, state);
+ const className: ?string = getViewTransitionClassName(
props.className,
- null,
- false,
+ state.paired ? props.share : props.enter,
);
- if (!inViewport) {
- // TODO: If this was part of a pair we will still run the onShare callback.
- // Revert the transition names. This boundary is not in the viewport
- // so we won't bother animating it.
- restoreViewTransitionOnHostInstances(placement.child, false);
- // TODO: Should we still visit the children in case a named one was in the viewport?
- } else {
- commitAppearingPairViewTransitions(placement);
+ if (className !== 'none') {
+ viewTransitionHostInstanceIdx = 0;
+ const inViewport = applyViewTransitionToHostInstances(
+ placement.child,
+ name,
+ className,
+ null,
+ false,
+ );
+ if (!inViewport) {
+ // TODO: If this was part of a pair we will still run the onShare callback.
+ // Revert the transition names. This boundary is not in the viewport
+ // so we won't bother animating it.
+ restoreViewTransitionOnHostInstances(placement.child, false);
+ // TODO: Should we still visit the children in case a named one was in the viewport?
+ } else {
+ commitAppearingPairViewTransitions(placement);
- const state: ViewTransitionState = placement.stateNode;
- if (!state.paired) {
- scheduleViewTransitionEvent(placement, props.onEnter);
+ if (!state.paired) {
+ scheduleViewTransitionEvent(placement, props.onEnter);
+ }
}
+ } else {
+ commitAppearingPairViewTransitions(placement);
}
} else if ((placement.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
let child = placement.child;
@@ -752,28 +770,34 @@ function commitDeletedPairViewTransitions(
if (name != null && name !== 'auto') {
const pair = appearingViewTransitions.get(name);
if (pair !== undefined) {
- // We found a new appearing view transition with the same name as this deletion.
- viewTransitionHostInstanceIdx = 0;
- const inViewport = applyViewTransitionToHostInstances(
- child.child,
- name,
+ const className: ?string = getViewTransitionClassName(
props.className,
- null,
- false,
+ props.share,
);
- if (!inViewport) {
- // This boundary is not in the viewport so we won't treat it as a matched pair.
- // Revert the transition names. This avoids it flying onto the screen which can
- // be disruptive and doesn't really preserve any continuity anyway.
- restoreViewTransitionOnHostInstances(child.child, false);
- } else {
- // We'll transition between them.
- const oldinstance: ViewTransitionState = child.stateNode;
- const newInstance: ViewTransitionState = pair;
- newInstance.paired = oldinstance;
- // Note: If the other side ends up outside the viewport, we'll still run this.
- // Therefore it's possible for onShare to be called with only an old snapshot.
- scheduleViewTransitionEvent(child, props.onShare);
+ if (className !== 'none') {
+ // We found a new appearing view transition with the same name as this deletion.
+ viewTransitionHostInstanceIdx = 0;
+ const inViewport = applyViewTransitionToHostInstances(
+ child.child,
+ name,
+ className,
+ null,
+ false,
+ );
+ if (!inViewport) {
+ // This boundary is not in the viewport so we won't treat it as a matched pair.
+ // Revert the transition names. This avoids it flying onto the screen which can
+ // be disruptive and doesn't really preserve any continuity anyway.
+ restoreViewTransitionOnHostInstances(child.child, false);
+ } else {
+ // We'll transition between them.
+ const oldinstance: ViewTransitionState = child.stateNode;
+ const newInstance: ViewTransitionState = pair;
+ newInstance.paired = oldinstance;
+ // Note: If the other side ends up outside the viewport, we'll still run this.
+ // Therefore it's possible for onShare to be called with only an old snapshot.
+ scheduleViewTransitionEvent(child, props.onShare);
+ }
}
// Delete the entry so that we know when we've found all of them
// and can stop searching (size reaches zero).
@@ -797,22 +821,29 @@ function commitExitViewTransitions(
if (deletion.tag === ViewTransitionComponent) {
const props: ViewTransitionProps = deletion.memoizedProps;
const name = getViewTransitionName(props, deletion.stateNode);
- viewTransitionHostInstanceIdx = 0;
- const inViewport = applyViewTransitionToHostInstances(
- deletion.child,
- name,
+ const pair =
+ appearingViewTransitions !== null
+ ? appearingViewTransitions.get(name)
+ : undefined;
+ const className: ?string = getViewTransitionClassName(
props.className,
- null,
- false,
+ pair !== undefined ? props.share : props.exit,
);
- if (!inViewport) {
- // Revert the transition names. This boundary is not in the viewport
- // so we won't bother animating it.
- restoreViewTransitionOnHostInstances(deletion.child, false);
- // TODO: Should we still visit the children in case a named one was in the viewport?
- } else if (appearingViewTransitions !== null) {
- const pair = appearingViewTransitions.get(name);
- if (pair !== undefined) {
+ if (className !== 'none') {
+ viewTransitionHostInstanceIdx = 0;
+ const inViewport = applyViewTransitionToHostInstances(
+ deletion.child,
+ name,
+ className,
+ null,
+ false,
+ );
+ if (!inViewport) {
+ // Revert the transition names. This boundary is not in the viewport
+ // so we won't bother animating it.
+ restoreViewTransitionOnHostInstances(deletion.child, false);
+ // TODO: Should we still visit the children in case a named one was in the viewport?
+ } else if (pair !== undefined) {
// We found a new appearing view transition with the same name as this deletion.
// We'll transition between them instead of running the normal exit.
const oldinstance: ViewTransitionState = deletion.stateNode;
@@ -820,6 +851,7 @@ function commitExitViewTransitions(
newInstance.paired = oldinstance;
// Delete the entry so that we know when we've found all of them
// and can stop searching (size reaches zero).
+ // $FlowFixMe[incompatible-use]: Refined by the pair.
appearingViewTransitions.delete(name);
// Note: If the other side ends up outside the viewport, we'll still run this.
// Therefore it's possible for onShare to be called with only an old snapshot.
@@ -827,10 +859,10 @@ function commitExitViewTransitions(
} else {
scheduleViewTransitionEvent(deletion, props.onExit);
}
+ }
+ if (appearingViewTransitions !== null) {
// Look for more pairs deeper in the tree.
commitDeletedPairViewTransitions(deletion, appearingViewTransitions);
- } else {
- scheduleViewTransitionEvent(deletion, props.onExit);
}
} else if ((deletion.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
let child = deletion.child;
@@ -845,7 +877,10 @@ function commitExitViewTransitions(
}
}
-function commitBeforeUpdateViewTransition(current: Fiber): void {
+function commitBeforeUpdateViewTransition(
+ current: Fiber,
+ finishedWork: Fiber,
+): void {
// The way we deal with multiple HostInstances as children of a View Transition in an
// update can get tricky. The important bit is that if you swap out n HostInstances
// from n HostInstances then they match up in order. Similarly, if you don't swap
@@ -862,13 +897,32 @@ function commitBeforeUpdateViewTransition(current: Fiber): void {
// be unexpected but it is in line with the semantics that the ViewTransition is its
// own layer that cross-fades its content when it updates. If you want to reorder then
// each child needs its own ViewTransition.
- const props: ViewTransitionProps = current.memoizedProps;
- const name = getViewTransitionName(props, current.stateNode);
+ const oldProps: ViewTransitionProps = current.memoizedProps;
+ const oldName = getViewTransitionName(oldProps, current.stateNode);
+ const newProps: ViewTransitionProps = finishedWork.memoizedProps;
+ // This className applies only if there are fewer child DOM nodes than
+ // before or if this update should've been cancelled but we ended up with
+ // a parent animating so we need to animate the child too.
+ // For example, if update="foo" layout="none" and it turns out this was
+ // a layout only change, then the "foo" class will be applied even though
+ // it was not actually an update. Which is a bug.
+ let className: ?string = getViewTransitionClassName(
+ newProps.className,
+ newProps.update,
+ );
+ if (className === 'none') {
+ className = getViewTransitionClassName(newProps.className, newProps.layout);
+ if (className === 'none') {
+ // If both update and layout are both "none" then we don't have to
+ // apply a name. Since we won't animate this boundary.
+ return;
+ }
+ }
viewTransitionHostInstanceIdx = 0;
applyViewTransitionToHostInstances(
current.child,
- name,
- props.className,
+ oldName,
+ className,
(current.memoizedState = []),
true,
);
@@ -882,14 +936,20 @@ function commitNestedViewTransitions(changedParent: Fiber): void {
// was an update through this component then the inner one wins.
const props: ViewTransitionProps = child.memoizedProps;
const name = getViewTransitionName(props, child.stateNode);
- viewTransitionHostInstanceIdx = 0;
- applyViewTransitionToHostInstances(
- child.child,
- name,
+ const className: ?string = getViewTransitionClassName(
props.className,
- (child.memoizedState = []),
- false,
+ props.layout,
);
+ if (className !== 'none') {
+ viewTransitionHostInstanceIdx = 0;
+ applyViewTransitionToHostInstances(
+ child.child,
+ name,
+ className,
+ (child.memoizedState = []),
+ false,
+ );
+ }
} else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
commitNestedViewTransitions(child);
}
@@ -979,10 +1039,58 @@ function restoreNestedViewTransitions(changedParent: Fiber): void {
}
}
+function cancelViewTransitionHostInstances(
+ currentViewTransition: Fiber,
+ child: null | Fiber,
+ stopAtNestedViewTransitions: boolean,
+): void {
+ if (!supportsMutation) {
+ return;
+ }
+ while (child !== null) {
+ if (child.tag === HostComponent) {
+ const instance: Instance = child.stateNode;
+ const oldName = getViewTransitionName(
+ currentViewTransition.memoizedProps,
+ currentViewTransition.stateNode,
+ );
+ if (viewTransitionCancelableChildren === null) {
+ viewTransitionCancelableChildren = [];
+ }
+ viewTransitionCancelableChildren.push(
+ instance,
+ oldName,
+ child.memoizedProps,
+ );
+ viewTransitionHostInstanceIdx++;
+ } else if (
+ child.tag === OffscreenComponent &&
+ child.memoizedState !== null
+ ) {
+ // Skip any hidden subtrees. They were or are effectively not there.
+ } else if (
+ child.tag === ViewTransitionComponent &&
+ stopAtNestedViewTransitions
+ ) {
+ // Skip any nested view transitions for updates since in that case the
+ // inner most one is the one that handles the update.
+ } else {
+ cancelViewTransitionHostInstances(
+ currentViewTransition,
+ child.child,
+ stopAtNestedViewTransitions,
+ );
+ }
+ child = child.sibling;
+ }
+}
+
function measureViewTransitionHostInstances(
currentViewTransition: Fiber,
parentViewTransition: Fiber,
child: null | Fiber,
+ name: string,
+ className: ?string,
previousMeasurements: null | Array,
stopAtNestedViewTransitions: boolean,
): boolean {
@@ -1028,20 +1136,15 @@ function measureViewTransitionHostInstances(
parentViewTransition.flags |= AffectedParentLayout;
}
if ((parentViewTransition.flags & Update) !== NoFlags) {
- const props: ViewTransitionProps = parentViewTransition.memoizedProps;
// We might update this node so we need to apply its new name for the new state.
- const newName = getViewTransitionName(
- props,
- parentViewTransition.stateNode,
- );
applyViewTransitionName(
instance,
viewTransitionHostInstanceIdx === 0
- ? newName
+ ? name
: // If we have multiple Host Instances below, we add a suffix to the name to give
// each one a unique name.
- newName + '_' + viewTransitionHostInstanceIdx,
- props.className,
+ name + '_' + viewTransitionHostInstanceIdx,
+ className,
);
}
if (!inViewport || (parentViewTransition.flags & Update) === NoFlags) {
@@ -1083,6 +1186,8 @@ function measureViewTransitionHostInstances(
currentViewTransition,
parentViewTransition,
child.child,
+ name,
+ className,
previousMeasurements,
stopAtNestedViewTransitions,
)
@@ -1099,6 +1204,42 @@ function measureUpdateViewTransition(
current: Fiber,
finishedWork: Fiber,
): boolean {
+ const props: ViewTransitionProps = finishedWork.memoizedProps;
+ const updateClassName: ?string = getViewTransitionClassName(
+ props.className,
+ props.update,
+ );
+ const layoutClassName: ?string = getViewTransitionClassName(
+ props.className,
+ props.update,
+ );
+ let className: ?string;
+ if (updateClassName === 'none') {
+ if (layoutClassName === 'none') {
+ // If both update and layout class name were none, then we didn't apply any
+ // names in the before update phase so we shouldn't now neither.
+ return false;
+ }
+ // We don't care if this is mutated or children layout changed, but we still
+ // measure each instance to see if it moved and therefore should apply layout.
+ finishedWork.flags &= ~Update;
+ className = layoutClassName;
+ } else if ((finishedWork.flags & Update) !== NoFlags) {
+ // It was updated and we have an appropriate class name to apply.
+ className = updateClassName;
+ } else {
+ if (layoutClassName === 'none') {
+ // If we did not update, then all changes are considered a layout. We'll
+ // attempt to cancel.
+ viewTransitionHostInstanceIdx = 0;
+ cancelViewTransitionHostInstances(current, finishedWork.child, true);
+ return false;
+ }
+ // We didn't update but we might still apply layout so we measure each
+ // instance to see if it moved or resized.
+ className = layoutClassName;
+ }
+ const name = getViewTransitionName(props, finishedWork.stateNode);
// If nothing changed due to a mutation, or children changing size
// and the measurements end up unchanged, we should restore it to not animate.
viewTransitionHostInstanceIdx = 0;
@@ -1107,6 +1248,8 @@ function measureUpdateViewTransition(
current,
finishedWork,
finishedWork.child,
+ name,
+ className,
previousMeasurements,
true,
);
@@ -1127,16 +1270,27 @@ function measureNestedViewTransitions(changedParent: Fiber): void {
if (child.tag === ViewTransitionComponent) {
const current = child.alternate;
if (current !== null) {
+ const props: ViewTransitionProps = child.memoizedProps;
+ const name = getViewTransitionName(props, child.stateNode);
+ const className: ?string = getViewTransitionClassName(
+ props.className,
+ props.layout,
+ );
viewTransitionHostInstanceIdx = 0;
- measureViewTransitionHostInstances(
+ const inViewport = measureViewTransitionHostInstances(
current,
child,
child.child,
+ name,
+ className,
child.memoizedState,
false,
);
- const props: ViewTransitionProps = child.memoizedProps;
- scheduleViewTransitionEvent(child, props.onLayout);
+ if ((child.flags & Update) === NoFlags || !inViewport) {
+ // Nothing changed.
+ } else {
+ scheduleViewTransitionEvent(child, props.onLayout);
+ }
}
} else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
measureNestedViewTransitions(child);
@@ -3010,11 +3164,6 @@ function recursivelyTraverseAfterMutationEffects(
// its size and position. We need to measure this and if not, restore it to
// not animate.
measureNestedViewTransitions(parentFiber);
- if ((parentFiber.flags & AffectedParentLayout) !== NoFlags) {
- // This boundary changed size in a way that may have caused its parent to
- // relayout. We need to bubble this information up to the parent.
- viewTransitionContextChanged = true;
- }
}
}
diff --git a/packages/react-reconciler/src/ReactFiberViewTransitionComponent.js b/packages/react-reconciler/src/ReactFiberViewTransitionComponent.js
index bfbc1b0b2e337..65e3ffc459148 100644
--- a/packages/react-reconciler/src/ReactFiberViewTransitionComponent.js
+++ b/packages/react-reconciler/src/ReactFiberViewTransitionComponent.js
@@ -19,8 +19,13 @@ import {getTreeId} from './ReactFiberTreeContext';
export type ViewTransitionProps = {
name?: string,
- className?: string,
children?: ReactNodeList,
+ className?: 'none' | string,
+ enter?: 'none' | string,
+ exit?: 'none' | string,
+ layout?: 'none' | string,
+ share?: 'none' | string,
+ update?: 'none' | string,
onEnter?: (instance: ViewTransitionInstance) => void,
onExit?: (instance: ViewTransitionInstance) => void,
onLayout?: (instance: ViewTransitionInstance) => void,
@@ -76,3 +81,19 @@ export function getViewTransitionName(
// We should have assigned a name by now.
return (instance.autoName: any);
}
+
+export function getViewTransitionClassName(
+ className: ?string,
+ eventClassName: ?string,
+): ?string {
+ if (eventClassName == null) {
+ return className;
+ }
+ if (eventClassName === 'none') {
+ return eventClassName;
+ }
+ if (className != null) {
+ return className + ' ' + eventClassName;
+ }
+ return eventClassName;
+}