diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
index 6b3ba6f94c8ae..a8efe8a4713eb 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
@@ -1445,6 +1445,7 @@ export enum ValueKind {
Primitive = 'primitive',
Global = 'global',
Mutable = 'mutable',
+ ShallowMutable = 'shallowmutable',
Context = 'context',
}
@@ -1454,6 +1455,7 @@ export const ValueKindSchema = z.enum([
ValueKind.Primitive,
ValueKind.Global,
ValueKind.Mutable,
+ ValueKind.ShallowMutable,
ValueKind.Context,
]);
diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts
index 8ef78aa196428..a9cd774e54574 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts
@@ -591,8 +591,14 @@ function applyEffect(
};
context.effectInstructionValueCache.set(effect, value);
}
+
+ const outputKind =
+ fromValue.kind === ValueKind.ShallowMutable
+ ? ValueKind.Frozen
+ : fromValue.kind;
+
state.initialize(value, {
- kind: fromValue.kind,
+ kind: outputKind,
reason: new Set(fromValue.reason),
});
state.define(effect.into, value);
@@ -607,10 +613,11 @@ function applyEffect(
});
break;
}
+ case ValueKind.ShallowMutable:
case ValueKind.Frozen: {
effects.push({
kind: 'Create',
- value: fromValue.kind,
+ value: outputKind,
into: effect.into,
reason: [...fromValue.reason][0] ?? ValueReason.Other,
});
@@ -720,11 +727,14 @@ function applyEffect(
* copy-on-write semantics, then we can prune the effect
*/
const intoKind = state.kind(effect.into).kind;
+ const fromKind = state.kind(effect.from).kind;
+
let isMutableDesination: boolean;
switch (intoKind) {
case ValueKind.Context:
case ValueKind.Mutable:
- case ValueKind.MaybeFrozen: {
+ case ValueKind.MaybeFrozen:
+ case ValueKind.ShallowMutable: {
isMutableDesination = true;
break;
}
@@ -733,7 +743,6 @@ function applyEffect(
break;
}
}
- const fromKind = state.kind(effect.from).kind;
let isMutableReferenceType: boolean;
switch (fromKind) {
case ValueKind.Global:
@@ -741,6 +750,7 @@ function applyEffect(
isMutableReferenceType = false;
break;
}
+ case ValueKind.ShallowMutable:
case ValueKind.Frozen: {
isMutableReferenceType = false;
applyEffect(
@@ -781,6 +791,7 @@ function applyEffect(
const fromValue = state.kind(effect.from);
const fromKind = fromValue.kind;
switch (fromKind) {
+ case ValueKind.ShallowMutable:
case ValueKind.Frozen: {
applyEffect(
context,
@@ -1267,6 +1278,7 @@ class InferenceState {
switch (value.kind) {
case ValueKind.Context:
case ValueKind.Mutable:
+ case ValueKind.ShallowMutable:
case ValueKind.MaybeFrozen: {
const values = this.values(place);
for (const instrValue of values) {
@@ -1315,13 +1327,30 @@ class InferenceState {
if (isRefOrRefValue(place.identifier)) {
return 'mutate-ref';
}
- const kind = this.kind(place).kind;
+ const abstractValue = this.kind(place);
+ const kind = abstractValue.kind;
+
+ // Downgrade ShallowMutable to Mutable when mutated
+ if (kind === ValueKind.ShallowMutable) {
+ const values = this.values(place);
+ for (const value of values) {
+ const valueInfo = this.#values.get(value);
+ if (valueInfo && valueInfo.kind === ValueKind.ShallowMutable) {
+ this.#values.set(value, {
+ kind: ValueKind.Mutable,
+ reason: valueInfo.reason,
+ });
+ }
+ }
+ }
+
switch (variant) {
case 'MutateConditionally':
case 'MutateTransitiveConditionally': {
switch (kind) {
case ValueKind.Mutable:
- case ValueKind.Context: {
+ case ValueKind.Context:
+ case ValueKind.ShallowMutable: {
return 'mutate';
}
default: {
@@ -1333,7 +1362,8 @@ class InferenceState {
case 'MutateTransitive': {
switch (kind) {
case ValueKind.Mutable:
- case ValueKind.Context: {
+ case ValueKind.Context:
+ case ValueKind.ShallowMutable: {
return 'mutate';
}
case ValueKind.Primitive: {
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/array-spread-from-hook.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/array-spread-from-hook.expect.md
new file mode 100644
index 0000000000000..4aa3346e24dfd
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/array-spread-from-hook.expect.md
@@ -0,0 +1,91 @@
+
+## Input
+
+```javascript
+import {useMemo} from 'react';
+import {ValidateMemoization} from 'shared-runtime';
+
+function useData() {
+ return ['a', 'b', 'c'];
+}
+
+function Component() {
+ const [first, ...rest] = useData();
+
+ const result = useMemo(() => {
+ return rest.join('-');
+ }, [rest]);
+
+ return ;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [],
+ isComponent: true,
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime";
+import { useMemo } from "react";
+import { ValidateMemoization } from "shared-runtime";
+
+function useData() {
+ const $ = _c(1);
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = ["a", "b", "c"];
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ return t0;
+}
+
+function Component() {
+ const $ = _c(7);
+ const t0 = useData();
+ let rest;
+ if ($[0] !== t0) {
+ [, ...rest] = t0;
+ $[0] = t0;
+ $[1] = rest;
+ } else {
+ rest = $[1];
+ }
+
+ const result = rest.join("-");
+ let t1;
+ if ($[2] !== rest) {
+ t1 = [rest];
+ $[2] = rest;
+ $[3] = t1;
+ } else {
+ t1 = $[3];
+ }
+ let t2;
+ if ($[4] !== result || $[5] !== t1) {
+ t2 = ;
+ $[4] = result;
+ $[5] = t1;
+ $[6] = t2;
+ } else {
+ t2 = $[6];
+ }
+ return t2;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [],
+ isComponent: true,
+};
+
+```
+
+### Eval output
+(kind: ok)
{"inputs":[["b","c"]],"output":"b-c"}
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/array-spread-from-hook.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/array-spread-from-hook.tsx
new file mode 100644
index 0000000000000..d1b0b40cda176
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/array-spread-from-hook.tsx
@@ -0,0 +1,22 @@
+import {useMemo} from 'react';
+import {ValidateMemoization} from 'shared-runtime';
+
+function useData() {
+ return ['a', 'b', 'c'];
+}
+
+function Component() {
+ const [first, ...rest] = useData();
+
+ const result = useMemo(() => {
+ return rest.join('-');
+ }, [rest]);
+
+ return ;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [],
+ isComponent: true,
+};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/destructured-with-rest-props.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/destructured-with-rest-props.expect.md
new file mode 100644
index 0000000000000..0720d98cddcf4
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/destructured-with-rest-props.expect.md
@@ -0,0 +1,120 @@
+
+## Input
+
+```javascript
+import {useMemo} from 'react';
+
+function useTheme() {
+ return {primary: '#blue', secondary: '#green'};
+}
+
+function computeStyles(
+ specialProp: string | undefined,
+ restProps: any,
+ theme: any,
+) {
+ return {
+ color: specialProp ? theme.primary : theme.secondary,
+ ...restProps.style,
+ };
+}
+
+export function SpecialButton({
+ specialProp,
+ ...restProps
+}: {
+ specialProp?: string;
+ style?: Record;
+ onClick?: () => void;
+}) {
+ const theme = useTheme();
+
+ const styles = useMemo(
+ () => computeStyles(specialProp, restProps, theme),
+ [specialProp, restProps, theme],
+ );
+
+ return (
+
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: SpecialButton,
+ params: [{specialProp: 'test', style: {fontSize: '16px'}, onClick: () => {}}],
+ isComponent: true,
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime";
+import { useMemo } from "react";
+
+function useTheme() {
+ const $ = _c(1);
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = { primary: "#blue", secondary: "#green" };
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ return t0;
+}
+
+function computeStyles(specialProp, restProps, theme) {
+ const $ = _c(3);
+
+ const t0 = specialProp ? theme.primary : theme.secondary;
+ let t1;
+ if ($[0] !== restProps.style || $[1] !== t0) {
+ t1 = { color: t0, ...restProps.style };
+ $[0] = restProps.style;
+ $[1] = t0;
+ $[2] = t1;
+ } else {
+ t1 = $[2];
+ }
+ return t1;
+}
+
+export function SpecialButton(t0) {
+ const $ = _c(3);
+ const { specialProp, ...restProps } = t0;
+
+ const theme = useTheme();
+
+ const styles = computeStyles(specialProp, restProps, theme);
+ let t1;
+ if ($[0] !== restProps.onClick || $[1] !== styles) {
+ t1 = (
+
+ );
+ $[0] = restProps.onClick;
+ $[1] = styles;
+ $[2] = t1;
+ } else {
+ t1 = $[2];
+ }
+ return t1;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: SpecialButton,
+ params: [
+ { specialProp: "test", style: { fontSize: "16px" }, onClick: () => {} },
+ ],
+ isComponent: true,
+};
+
+```
+
+### Eval output
+(kind: ok)
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/destructured-with-rest-props.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/destructured-with-rest-props.tsx
new file mode 100644
index 0000000000000..c0804c824b83f
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/destructured-with-rest-props.tsx
@@ -0,0 +1,44 @@
+import {useMemo} from 'react';
+
+function useTheme() {
+ return {primary: '#blue', secondary: '#green'};
+}
+
+function computeStyles(
+ specialProp: string | undefined,
+ restProps: any,
+ theme: any,
+) {
+ return {
+ color: specialProp ? theme.primary : theme.secondary,
+ ...restProps.style,
+ };
+}
+
+export function SpecialButton({
+ specialProp,
+ ...restProps
+}: {
+ specialProp?: string;
+ style?: Record;
+ onClick?: () => void;
+}) {
+ const theme = useTheme();
+
+ const styles = useMemo(
+ () => computeStyles(specialProp, restProps, theme),
+ [specialProp, restProps, theme],
+ );
+
+ return (
+
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: SpecialButton,
+ params: [{specialProp: 'test', style: {fontSize: '16px'}, onClick: () => {}}],
+ isComponent: true,
+};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/function-arg-spread.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/function-arg-spread.expect.md
new file mode 100644
index 0000000000000..7162bc1fdf9f0
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/function-arg-spread.expect.md
@@ -0,0 +1,85 @@
+
+## Input
+
+```javascript
+import {useMemo} from 'react';
+import {ValidateMemoization} from 'shared-runtime';
+
+function Component({...data}: {x: number; y: number}) {
+ const result = useMemo(() => {
+ return data.x + data.y;
+ }, [data.x, data.y]);
+
+ return ;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{x: 10, y: 20}],
+ sequentialRenders: [
+ {x: 10, y: 20},
+ {x: 10, y: 20},
+ {x: 15, y: 25},
+ ],
+ isComponent: true,
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime";
+import { useMemo } from "react";
+import { ValidateMemoization } from "shared-runtime";
+
+function Component(t0) {
+ const $ = _c(8);
+ let data;
+ if ($[0] !== t0) {
+ ({ ...data } = t0);
+ $[0] = t0;
+ $[1] = data;
+ } else {
+ data = $[1];
+ }
+ const result = data.x + data.y;
+ let t1;
+ if ($[2] !== data.x || $[3] !== data.y) {
+ t1 = [data.x, data.y];
+ $[2] = data.x;
+ $[3] = data.y;
+ $[4] = t1;
+ } else {
+ t1 = $[4];
+ }
+ let t2;
+ if ($[5] !== result || $[6] !== t1) {
+ t2 = ;
+ $[5] = result;
+ $[6] = t1;
+ $[7] = t2;
+ } else {
+ t2 = $[7];
+ }
+ return t2;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{ x: 10, y: 20 }],
+ sequentialRenders: [
+ { x: 10, y: 20 },
+ { x: 10, y: 20 },
+ { x: 15, y: 25 },
+ ],
+
+ isComponent: true,
+};
+
+```
+
+### Eval output
+(kind: ok) {"inputs":[10,20],"output":30}
+{"inputs":[10,20],"output":30}
+{"inputs":[15,25],"output":40}
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/function-arg-spread.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/function-arg-spread.tsx
new file mode 100644
index 0000000000000..517f75f0e7ec2
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/function-arg-spread.tsx
@@ -0,0 +1,21 @@
+import {useMemo} from 'react';
+import {ValidateMemoization} from 'shared-runtime';
+
+function Component({...data}: {x: number; y: number}) {
+ const result = useMemo(() => {
+ return data.x + data.y;
+ }, [data.x, data.y]);
+
+ return ;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{x: 10, y: 20}],
+ sequentialRenders: [
+ {x: 10, y: 20},
+ {x: 10, y: 20},
+ {x: 15, y: 25},
+ ],
+ isComponent: true,
+};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/object-spread-hook-returns.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/object-spread-hook-returns.expect.md
new file mode 100644
index 0000000000000..9e6b273460b37
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/object-spread-hook-returns.expect.md
@@ -0,0 +1,98 @@
+
+## Input
+
+```javascript
+import {useMemo} from 'react';
+import {ValidateMemoization} from 'shared-runtime';
+
+function useConfig() {
+ return {a: 1, b: 2, c: 3};
+}
+
+function Component() {
+ const {...spread} = useConfig();
+
+ const result = useMemo(() => {
+ return spread.a + spread.b + spread.c;
+ }, [spread.a, spread.b, spread.c]);
+
+ return (
+
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [],
+ isComponent: true,
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime";
+import { useMemo } from "react";
+import { ValidateMemoization } from "shared-runtime";
+
+function useConfig() {
+ const $ = _c(1);
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = { a: 1, b: 2, c: 3 };
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ return t0;
+}
+
+function Component() {
+ const $ = _c(9);
+ const t0 = useConfig();
+ let spread;
+ if ($[0] !== t0) {
+ ({ ...spread } = t0);
+ $[0] = t0;
+ $[1] = spread;
+ } else {
+ spread = $[1];
+ }
+
+ const result = spread.a + spread.b + spread.c;
+ let t1;
+ if ($[2] !== spread.a || $[3] !== spread.b || $[4] !== spread.c) {
+ t1 = [spread.a, spread.b, spread.c];
+ $[2] = spread.a;
+ $[3] = spread.b;
+ $[4] = spread.c;
+ $[5] = t1;
+ } else {
+ t1 = $[5];
+ }
+ let t2;
+ if ($[6] !== result || $[7] !== t1) {
+ t2 = ;
+ $[6] = result;
+ $[7] = t1;
+ $[8] = t2;
+ } else {
+ t2 = $[8];
+ }
+ return t2;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [],
+ isComponent: true,
+};
+
+```
+
+### Eval output
+(kind: ok) {"inputs":[1,2,3],"output":6}
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/object-spread-hook-returns.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/object-spread-hook-returns.tsx
new file mode 100644
index 0000000000000..1136d2ff303c2
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/object-spread-hook-returns.tsx
@@ -0,0 +1,27 @@
+import {useMemo} from 'react';
+import {ValidateMemoization} from 'shared-runtime';
+
+function useConfig() {
+ return {a: 1, b: 2, c: 3};
+}
+
+function Component() {
+ const {...spread} = useConfig();
+
+ const result = useMemo(() => {
+ return spread.a + spread.b + spread.c;
+ }, [spread.a, spread.b, spread.c]);
+
+ return (
+
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [],
+ isComponent: true,
+};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/regular-props.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/regular-props.expect.md
new file mode 100644
index 0000000000000..caa60991a8243
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/regular-props.expect.md
@@ -0,0 +1,95 @@
+
+## Input
+
+```javascript
+import {useMemo} from 'react';
+
+function useSession() {
+ return {user: {userCode: 'ABC123'}};
+}
+
+function getDefaultFromValue(
+ defaultValues: string | undefined,
+ userCode: string,
+) {
+ return defaultValues ? `${defaultValues}-${userCode}` : userCode;
+}
+
+export function UpSertField(props: {defaultValues?: string}) {
+ const {
+ user: {userCode},
+ } = useSession();
+
+ const defaultValues = useMemo(
+ () => getDefaultFromValue(props.defaultValues, userCode),
+ [props.defaultValues, userCode],
+ );
+
+ return {defaultValues}
;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: UpSertField,
+ params: [{defaultValues: 'test'}],
+ isComponent: true,
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime";
+import { useMemo } from "react";
+
+function useSession() {
+ const $ = _c(1);
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = { user: { userCode: "ABC123" } };
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ return t0;
+}
+
+function getDefaultFromValue(defaultValues, userCode) {
+ return defaultValues ? `${defaultValues}-${userCode}` : userCode;
+}
+
+export function UpSertField(props) {
+ const $ = _c(5);
+ const { user: t0 } = useSession();
+ const { userCode } = t0;
+ let t1;
+ if ($[0] !== props.defaultValues || $[1] !== userCode) {
+ t1 = getDefaultFromValue(props.defaultValues, userCode);
+ $[0] = props.defaultValues;
+ $[1] = userCode;
+ $[2] = t1;
+ } else {
+ t1 = $[2];
+ }
+ const defaultValues = t1;
+ let t2;
+ if ($[3] !== defaultValues) {
+ t2 = {defaultValues}
;
+ $[3] = defaultValues;
+ $[4] = t2;
+ } else {
+ t2 = $[4];
+ }
+ return t2;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: UpSertField,
+ params: [{ defaultValues: "test" }],
+ isComponent: true,
+};
+
+```
+
+### Eval output
+(kind: ok) test-ABC123
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/regular-props.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/regular-props.tsx
new file mode 100644
index 0000000000000..693d09484266d
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/regular-props.tsx
@@ -0,0 +1,31 @@
+import {useMemo} from 'react';
+
+function useSession() {
+ return {user: {userCode: 'ABC123'}};
+}
+
+function getDefaultFromValue(
+ defaultValues: string | undefined,
+ userCode: string,
+) {
+ return defaultValues ? `${defaultValues}-${userCode}` : userCode;
+}
+
+export function UpSertField(props: {defaultValues?: string}) {
+ const {
+ user: {userCode},
+ } = useSession();
+
+ const defaultValues = useMemo(
+ () => getDefaultFromValue(props.defaultValues, userCode),
+ [props.defaultValues, userCode],
+ );
+
+ return {defaultValues}
;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: UpSertField,
+ params: [{defaultValues: 'test'}],
+ isComponent: true,
+};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/shallow-mutable-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/shallow-mutable-mutation.expect.md
new file mode 100644
index 0000000000000..7728ab7a6ba4d
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/shallow-mutable-mutation.expect.md
@@ -0,0 +1,58 @@
+
+## Input
+
+```javascript
+function Component({...props}: {value: string}) {
+ const obj = {};
+ props.newProp = obj;
+ obj.mutated = true;
+
+ return {props.value}
;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{value: 'test'}],
+ isComponent: true,
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime";
+function Component(t0) {
+ const $ = _c(4);
+ let props;
+ if ($[0] !== t0) {
+ ({ ...props } = t0);
+ const obj = {};
+ props.newProp = obj;
+ obj.mutated = true;
+ $[0] = t0;
+ $[1] = props;
+ } else {
+ props = $[1];
+ }
+ let t1;
+ if ($[2] !== props.value) {
+ t1 = {props.value}
;
+ $[2] = props.value;
+ $[3] = t1;
+ } else {
+ t1 = $[3];
+ }
+ return t1;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{ value: "test" }],
+ isComponent: true,
+};
+
+```
+
+### Eval output
+(kind: ok) test
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/shallow-mutable-mutation.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/shallow-mutable-mutation.tsx
new file mode 100644
index 0000000000000..f329d9da959b4
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/shallow-mutable-mutation.tsx
@@ -0,0 +1,13 @@
+function Component({...props}: {value: string}) {
+ const obj = {};
+ props.newProp = obj;
+ obj.mutated = true;
+
+ return {props.value}
;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{value: 'test'}],
+ isComponent: true,
+};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/spread-props.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/spread-props.expect.md
new file mode 100644
index 0000000000000..b1bd8d542c51d
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/spread-props.expect.md
@@ -0,0 +1,84 @@
+
+## Input
+
+```javascript
+import {useMemo} from 'react';
+import {ValidateMemoization} from 'shared-runtime';
+
+function Component({...props}: {value: string}) {
+ const result = useMemo(() => {
+ return props.value.toUpperCase();
+ }, [props.value]);
+
+ return ;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{value: 'test'}],
+ sequentialRenders: [{value: 'test'}, {value: 'test'}, {value: 'changed'}],
+ isComponent: true,
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime";
+import { useMemo } from "react";
+import { ValidateMemoization } from "shared-runtime";
+
+function Component(t0) {
+ const $ = _c(8);
+ let props;
+ let t1;
+ if ($[0] !== t0) {
+ ({ ...props } = t0);
+
+ t1 = props.value.toUpperCase();
+ $[0] = t0;
+ $[1] = props;
+ $[2] = t1;
+ } else {
+ props = $[1];
+ t1 = $[2];
+ }
+ const result = t1;
+ let t2;
+ if ($[3] !== props.value) {
+ t2 = [props.value];
+ $[3] = props.value;
+ $[4] = t2;
+ } else {
+ t2 = $[4];
+ }
+ let t3;
+ if ($[5] !== result || $[6] !== t2) {
+ t3 = ;
+ $[5] = result;
+ $[6] = t2;
+ $[7] = t3;
+ } else {
+ t3 = $[7];
+ }
+ return t3;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{ value: "test" }],
+ sequentialRenders: [
+ { value: "test" },
+ { value: "test" },
+ { value: "changed" },
+ ],
+ isComponent: true,
+};
+
+```
+
+### Eval output
+(kind: ok) {"inputs":["test"],"output":"TEST"}
+{"inputs":["test"],"output":"TEST"}
+{"inputs":["changed"],"output":"CHANGED"}
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/spread-props.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/spread-props.tsx
new file mode 100644
index 0000000000000..7b7f831fe3e10
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/spread-props.tsx
@@ -0,0 +1,17 @@
+import {useMemo} from 'react';
+import {ValidateMemoization} from 'shared-runtime';
+
+function Component({...props}: {value: string}) {
+ const result = useMemo(() => {
+ return props.value.toUpperCase();
+ }, [props.value]);
+
+ return ;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{value: 'test'}],
+ sequentialRenders: [{value: 'test'}, {value: 'test'}, {value: 'changed'}],
+ isComponent: true,
+};