diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts index c3eadb89f50..264174debaf 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts @@ -114,6 +114,99 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [ returnValueKind: ValueKind.Mutable, }), ], + [ + 'entries', + addFunction(DEFAULT_SHAPES, [], { + positionalParams: [Effect.Capture], + restParam: null, + returnType: {kind: 'Object', shapeId: BuiltInArrayId}, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Mutable, + aliasing: { + receiver: '@receiver', + params: ['@object'], + rest: null, + returns: '@returns', + temporaries: [], + effects: [ + { + kind: 'Create', + into: '@returns', + reason: ValueReason.KnownReturnSignature, + value: ValueKind.Mutable, + }, + // Object values are captured into the return + { + kind: 'Capture', + from: '@object', + into: '@returns', + }, + ], + }, + }), + ], + [ + 'keys', + addFunction(DEFAULT_SHAPES, [], { + positionalParams: [Effect.Read], + restParam: null, + returnType: {kind: 'Object', shapeId: BuiltInArrayId}, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Mutable, + aliasing: { + receiver: '@receiver', + params: ['@object'], + rest: null, + returns: '@returns', + temporaries: [], + effects: [ + { + kind: 'Create', + into: '@returns', + reason: ValueReason.KnownReturnSignature, + value: ValueKind.Mutable, + }, + // Only keys are captured, and keys are immutable + { + kind: 'ImmutableCapture', + from: '@object', + into: '@returns', + }, + ], + }, + }), + ], + [ + 'values', + addFunction(DEFAULT_SHAPES, [], { + positionalParams: [Effect.Capture], + restParam: null, + returnType: {kind: 'Object', shapeId: BuiltInArrayId}, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Mutable, + aliasing: { + receiver: '@receiver', + params: ['@object'], + rest: null, + returns: '@returns', + temporaries: [], + effects: [ + { + kind: 'Create', + into: '@returns', + reason: ValueReason.KnownReturnSignature, + value: ValueKind.Mutable, + }, + // Object values are captured into the return + { + kind: 'Capture', + from: '@object', + into: '@returns', + }, + ], + }, + }), + ], ]), ], [ diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts index eaf728db951..ced080adcc0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts @@ -142,6 +142,7 @@ function parseAliasingSignatureConfig( const effects = typeConfig.effects.map( (effect: AliasingEffectConfig): AliasingEffect => { switch (effect.kind) { + case 'ImmutableCapture': case 'CreateFrom': case 'Capture': case 'Alias': diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts index 5945e3a0782..e63ef067b20 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts @@ -111,6 +111,19 @@ export const AliasEffectSchema: z.ZodType = z.object({ into: LifetimeIdSchema, }); +export type ImmutableCaptureEffectConfig = { + kind: 'ImmutableCapture'; + from: string; + into: string; +}; + +export const ImmutableCaptureEffectSchema: z.ZodType = + z.object({ + kind: z.literal('ImmutableCapture'), + from: LifetimeIdSchema, + into: LifetimeIdSchema, + }); + export type CaptureEffectConfig = { kind: 'Capture'; from: string; @@ -187,6 +200,7 @@ export type AliasingEffectConfig = | AssignEffectConfig | AliasEffectConfig | CaptureEffectConfig + | ImmutableCaptureEffectConfig | ImpureEffectConfig | MutateEffectConfig | MutateTransitiveConditionallyConfig @@ -199,6 +213,7 @@ export const AliasingEffectSchema: z.ZodType = z.union([ AssignEffectSchema, AliasEffectSchema, CaptureEffectSchema, + ImmutableCaptureEffectSchema, ImpureEffectSchema, MutateEffectSchema, MutateTransitiveConditionallySchema, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.validate-object-entries-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.validate-object-entries-mutation.expect.md new file mode 100644 index 00000000000..09ff6e7214b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.validate-object-entries-mutation.expect.md @@ -0,0 +1,57 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import {makeObject_Primitives, Stringify} from 'shared-runtime'; + +function Component(props) { + const object = {object: props.object}; + const entries = useMemo(() => Object.entries(object), [object]); + entries.map(([, value]) => { + value.updated = true; + }); + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{object: {key: makeObject_Primitives()}}], +}; + +``` + + +## Error + +``` +Found 2 errors: + +Memoization: Compilation skipped because existing memoization could not be preserved + +React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly. + +error.validate-object-entries-mutation.ts:6:57 + 4 | function Component(props) { + 5 | const object = {object: props.object}; +> 6 | const entries = useMemo(() => Object.entries(object), [object]); + | ^^^^^^ This dependency may be modified later + 7 | entries.map(([, value]) => { + 8 | value.updated = true; + 9 | }); + +Memoization: Compilation skipped because existing memoization could not be preserved + +React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. + +error.validate-object-entries-mutation.ts:6:18 + 4 | function Component(props) { + 5 | const object = {object: props.object}; +> 6 | const entries = useMemo(() => Object.entries(object), [object]); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not preserve existing memoization + 7 | entries.map(([, value]) => { + 8 | value.updated = true; + 9 | }); +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.validate-object-entries-mutation.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.validate-object-entries-mutation.js new file mode 100644 index 00000000000..b4145b1617f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.validate-object-entries-mutation.js @@ -0,0 +1,16 @@ +// @validatePreserveExistingMemoizationGuarantees +import {makeObject_Primitives, Stringify} from 'shared-runtime'; + +function Component(props) { + const object = {object: props.object}; + const entries = useMemo(() => Object.entries(object), [object]); + entries.map(([, value]) => { + value.updated = true; + }); + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{object: {key: makeObject_Primitives()}}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.validate-object-values-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.validate-object-values-mutation.expect.md new file mode 100644 index 00000000000..b791b629278 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.validate-object-values-mutation.expect.md @@ -0,0 +1,57 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import {makeObject_Primitives, Stringify} from 'shared-runtime'; + +function Component(props) { + const object = {object: props.object}; + const values = useMemo(() => Object.values(object), [object]); + values.map(value => { + value.updated = true; + }); + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{object: {key: makeObject_Primitives()}}], +}; + +``` + + +## Error + +``` +Found 2 errors: + +Memoization: Compilation skipped because existing memoization could not be preserved + +React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly. + +error.validate-object-values-mutation.ts:6:55 + 4 | function Component(props) { + 5 | const object = {object: props.object}; +> 6 | const values = useMemo(() => Object.values(object), [object]); + | ^^^^^^ This dependency may be modified later + 7 | values.map(value => { + 8 | value.updated = true; + 9 | }); + +Memoization: Compilation skipped because existing memoization could not be preserved + +React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. + +error.validate-object-values-mutation.ts:6:17 + 4 | function Component(props) { + 5 | const object = {object: props.object}; +> 6 | const values = useMemo(() => Object.values(object), [object]); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not preserve existing memoization + 7 | values.map(value => { + 8 | value.updated = true; + 9 | }); +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.validate-object-values-mutation.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.validate-object-values-mutation.js new file mode 100644 index 00000000000..3482887d920 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.validate-object-values-mutation.js @@ -0,0 +1,16 @@ +// @validatePreserveExistingMemoizationGuarantees +import {makeObject_Primitives, Stringify} from 'shared-runtime'; + +function Component(props) { + const object = {object: props.object}; + const values = useMemo(() => Object.values(object), [object]); + values.map(value => { + value.updated = true; + }); + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{object: {key: makeObject_Primitives()}}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-entries-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-entries-mutation.expect.md new file mode 100644 index 00000000000..bc541b47f1f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-entries-mutation.expect.md @@ -0,0 +1,57 @@ + +## Input + +```javascript +import {makeObject_Primitives, Stringify} from 'shared-runtime'; + +function Component(props) { + const object = {object: props.object}; + const entries = Object.entries(object); + entries.map(([, value]) => { + value.updated = true; + }); + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{object: {key: makeObject_Primitives()}}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { makeObject_Primitives, Stringify } from "shared-runtime"; + +function Component(props) { + const $ = _c(2); + let t0; + if ($[0] !== props.object) { + const object = { object: props.object }; + const entries = Object.entries(object); + entries.map(_temp); + t0 = ; + $[0] = props.object; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; +} +function _temp(t0) { + const [, value] = t0; + value.updated = true; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ object: { key: makeObject_Primitives() } }], +}; + +``` + +### Eval output +(kind: ok)
{"entries":[["object",{"key":{"a":0,"b":"value1","c":true},"updated":true}]]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-entries-mutation.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-entries-mutation.js new file mode 100644 index 00000000000..2902cffd014 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-entries-mutation.js @@ -0,0 +1,15 @@ +import {makeObject_Primitives, Stringify} from 'shared-runtime'; + +function Component(props) { + const object = {object: props.object}; + const entries = Object.entries(object); + entries.map(([, value]) => { + value.updated = true; + }); + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{object: {key: makeObject_Primitives()}}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-keys.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-keys.expect.md new file mode 100644 index 00000000000..f076d9032db --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-keys.expect.md @@ -0,0 +1,108 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; +import {Stringify} from 'shared-runtime'; + +// derived from https://github.com/facebook/react/issues/32261 +function Component({items}) { + const record = useMemo( + () => + Object.fromEntries( + items.map(item => [item.id, ref => ]) + ), + [items] + ); + + // Without a declaration for Object.entries(), this would be assumed to mutate + // `record`, meaning existing memoization couldn't be preserved + return ( +
+ {Object.keys(record).map(id => ( + + ))} +
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + items: [ + {id: '0', name: 'Hello'}, + {id: '1', name: 'World!'}, + ], + }, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { Stringify } from "shared-runtime"; + +// derived from https://github.com/facebook/react/issues/32261 +function Component(t0) { + const $ = _c(7); + const { items } = t0; + let t1; + if ($[0] !== items) { + t1 = Object.fromEntries(items.map(_temp)); + $[0] = items; + $[1] = t1; + } else { + t1 = $[1]; + } + const record = t1; + let t2; + if ($[2] !== record) { + t2 = Object.keys(record); + $[2] = record; + $[3] = t2; + } else { + t2 = $[3]; + } + let t3; + if ($[4] !== record || $[5] !== t2) { + t3 = ( +
+ {t2.map((id) => ( + + ))} +
+ ); + $[4] = record; + $[5] = t2; + $[6] = t3; + } else { + t3 = $[6]; + } + return t3; +} +function _temp(item) { + return [item.id, (ref) => ]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + items: [ + { id: "0", name: "Hello" }, + { id: "1", name: "World!" }, + ], + }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"render":"[[ function params=1 ]]"}
{"render":"[[ function params=1 ]]"}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-keys.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-keys.js new file mode 100644 index 00000000000..38ae97ab956 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-keys.js @@ -0,0 +1,36 @@ +// @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; +import {Stringify} from 'shared-runtime'; + +// derived from https://github.com/facebook/react/issues/32261 +function Component({items}) { + const record = useMemo( + () => + Object.fromEntries( + items.map(item => [item.id, ref => ]) + ), + [items] + ); + + // Without a declaration for Object.entries(), this would be assumed to mutate + // `record`, meaning existing memoization couldn't be preserved + return ( +
+ {Object.keys(record).map(id => ( + + ))} +
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + items: [ + {id: '0', name: 'Hello'}, + {id: '1', name: 'World!'}, + ], + }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values-mutation.expect.md new file mode 100644 index 00000000000..bc541b47f1f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values-mutation.expect.md @@ -0,0 +1,57 @@ + +## Input + +```javascript +import {makeObject_Primitives, Stringify} from 'shared-runtime'; + +function Component(props) { + const object = {object: props.object}; + const entries = Object.entries(object); + entries.map(([, value]) => { + value.updated = true; + }); + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{object: {key: makeObject_Primitives()}}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { makeObject_Primitives, Stringify } from "shared-runtime"; + +function Component(props) { + const $ = _c(2); + let t0; + if ($[0] !== props.object) { + const object = { object: props.object }; + const entries = Object.entries(object); + entries.map(_temp); + t0 = ; + $[0] = props.object; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; +} +function _temp(t0) { + const [, value] = t0; + value.updated = true; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ object: { key: makeObject_Primitives() } }], +}; + +``` + +### Eval output +(kind: ok)
{"entries":[["object",{"key":{"a":0,"b":"value1","c":true},"updated":true}]]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values-mutation.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values-mutation.js new file mode 100644 index 00000000000..2902cffd014 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values-mutation.js @@ -0,0 +1,15 @@ +import {makeObject_Primitives, Stringify} from 'shared-runtime'; + +function Component(props) { + const object = {object: props.object}; + const entries = Object.entries(object); + entries.map(([, value]) => { + value.updated = true; + }); + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{object: {key: makeObject_Primitives()}}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values.expect.md new file mode 100644 index 00000000000..90039906641 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values.expect.md @@ -0,0 +1,103 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; +import {Stringify} from 'shared-runtime'; + +// derived from https://github.com/facebook/react/issues/32261 +function Component({items}) { + const record = useMemo( + () => + Object.fromEntries( + items.map(item => [ + item.id, + {id: item.id, render: ref => }, + ]) + ), + [items] + ); + + // Without a declaration for Object.entries(), this would be assumed to mutate + // `record`, meaning existing memoization couldn't be preserved + return ( +
+ {Object.values(record).map(({id, render}) => ( + + ))} +
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + items: [ + {id: '0', name: 'Hello'}, + {id: '1', name: 'World!'}, + ], + }, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { Stringify } from "shared-runtime"; + +// derived from https://github.com/facebook/react/issues/32261 +function Component(t0) { + const $ = _c(4); + const { items } = t0; + let t1; + if ($[0] !== items) { + t1 = Object.fromEntries(items.map(_temp)); + $[0] = items; + $[1] = t1; + } else { + t1 = $[1]; + } + const record = t1; + let t2; + if ($[2] !== record) { + t2 =
{Object.values(record).map(_temp2)}
; + $[2] = record; + $[3] = t2; + } else { + t2 = $[3]; + } + return t2; +} +function _temp2(t0) { + const { id, render } = t0; + return ; +} +function _temp(item) { + return [ + item.id, + { id: item.id, render: (ref) => }, + ]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + items: [ + { id: "0", name: "Hello" }, + { id: "1", name: "World!" }, + ], + }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"render":"[[ function params=1 ]]"}
{"render":"[[ function params=1 ]]"}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values.js new file mode 100644 index 00000000000..4cf229c379a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values.js @@ -0,0 +1,39 @@ +// @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; +import {Stringify} from 'shared-runtime'; + +// derived from https://github.com/facebook/react/issues/32261 +function Component({items}) { + const record = useMemo( + () => + Object.fromEntries( + items.map(item => [ + item.id, + {id: item.id, render: ref => }, + ]) + ), + [items] + ); + + // Without a declaration for Object.entries(), this would be assumed to mutate + // `record`, meaning existing memoization couldn't be preserved + return ( +
+ {Object.values(record).map(({id, render}) => ( + + ))} +
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + items: [ + {id: '0', name: 'Hello'}, + {id: '1', name: 'World!'}, + ], + }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-object-fromEntries-entries.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-object-fromEntries-entries.expect.md new file mode 100644 index 00000000000..b7ebcbc1cf6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-object-fromEntries-entries.expect.md @@ -0,0 +1,97 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; +import {Stringify} from 'shared-runtime'; + +// derived from https://github.com/facebook/react/issues/32261 +function Component({items}) { + const record = useMemo( + () => + Object.fromEntries( + items.map(item => [item.id, ref => ]) + ), + [items] + ); + + // Without a declaration for Object.entries(), this would be assumed to mutate + // `record`, meaning existing memoization couldn't be preserved + return ( +
+ {Object.entries(record).map(([id, render]) => ( + + ))} +
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + items: [ + {id: '0', name: 'Hello'}, + {id: '1', name: 'World!'}, + ], + }, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { Stringify } from "shared-runtime"; + +// derived from https://github.com/facebook/react/issues/32261 +function Component(t0) { + const $ = _c(4); + const { items } = t0; + let t1; + if ($[0] !== items) { + t1 = Object.fromEntries(items.map(_temp)); + $[0] = items; + $[1] = t1; + } else { + t1 = $[1]; + } + const record = t1; + let t2; + if ($[2] !== record) { + t2 =
{Object.entries(record).map(_temp2)}
; + $[2] = record; + $[3] = t2; + } else { + t2 = $[3]; + } + return t2; +} +function _temp2(t0) { + const [id, render] = t0; + return ; +} +function _temp(item) { + return [item.id, (ref) => ]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + items: [ + { id: "0", name: "Hello" }, + { id: "1", name: "World!" }, + ], + }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"render":"[[ function params=1 ]]"}
{"render":"[[ function params=1 ]]"}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-object-fromEntries-entries.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-object-fromEntries-entries.js new file mode 100644 index 00000000000..7dd8f34826a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-object-fromEntries-entries.js @@ -0,0 +1,36 @@ +// @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; +import {Stringify} from 'shared-runtime'; + +// derived from https://github.com/facebook/react/issues/32261 +function Component({items}) { + const record = useMemo( + () => + Object.fromEntries( + items.map(item => [item.id, ref => ]) + ), + [items] + ); + + // Without a declaration for Object.entries(), this would be assumed to mutate + // `record`, meaning existing memoization couldn't be preserved + return ( +
+ {Object.entries(record).map(([id, render]) => ( + + ))} +
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + items: [ + {id: '0', name: 'Hello'}, + {id: '1', name: 'World!'}, + ], + }, + ], +}; diff --git a/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts b/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts index 618d3b78b9d..730b6ff6f85 100644 --- a/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts +++ b/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts @@ -101,7 +101,7 @@ const COMPILER_OPTIONS: Partial = { // Don't emit errors on Flow suppressions--Flow already gave a signal flowSuppressions: false, environment: validateEnvironmentConfig({ - validateRefAccessDuringRender: false, + validateRefAccessDuringRender: true, validateNoSetStateInRender: true, validateNoSetStateInEffects: true, validateNoJSXInTryStatements: true, diff --git a/packages/eslint-plugin-react-hooks/src/rules/ReactCompiler.ts b/packages/eslint-plugin-react-hooks/src/rules/ReactCompiler.ts index 8961561ef96..795a117981a 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/ReactCompiler.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/ReactCompiler.ts @@ -103,7 +103,7 @@ const COMPILER_OPTIONS: Partial = { // Don't emit errors on Flow suppressions--Flow already gave a signal flowSuppressions: false, environment: validateEnvironmentConfig({ - validateRefAccessDuringRender: false, + validateRefAccessDuringRender: true, validateNoSetStateInRender: true, validateNoSetStateInEffects: true, validateNoJSXInTryStatements: true,