Compare commits

..

6 Commits

Author SHA1 Message Date
Joe Savona
75be876f2a [compiler] Add definitions for Object entries/keys/values
Fixes remaining issue in #32261, where passing a previously useMemo()-d value to `Object.entries()` makes the compiler think the value is mutated and fail validatePreserveExistingMemo. While I was there I added Object.keys() and Object.values() too.
2025-07-29 21:58:58 -07:00
Joe Savona
7d696dc3b8 Enable ref validation in linter 2025-07-29 12:22:05 -07:00
Joseph Savona
04a7a61918 [compiler] Allow assigning ref-accessing functions to objects if not mutated (#34026)
Allows assigning a ref-accessing function to an object so long as that
object is not subsequently transitively mutated. We should likely
rewrite the ref validation to use the new mutation/aliasing effects,
which would provide a more consistent behavior across instruction types
and require fewer special cases like this.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34026).
* #34027
* __->__ #34026
2025-07-29 10:57:26 -07:00
Joseph Savona
c2326b1336 [compiler] disallow ref access in state initializer, reducer/initializer (#34025)
Per title, disallow ref access in `useState()` initializer function,
`useReducer()` reducer, and `useReducer()` init function.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34025).
* #34027
* #34026
* __->__ #34025
2025-07-29 10:56:04 -07:00
Joseph Savona
4395689980 [compiler] ref guards apply up to fallthrough of the test (#34024)
Fixes #30782

When developers do an `if (ref.current == null)` guard for lazy ref
initialization, the "safe" blocks should extend up to the if's
fallthrough. Previously we only allowed writing to the ref in the if
consequent, but this meant that you couldn't use a ternary, logical, etc
in the if body.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34024).
* #34027
* #34026
* #34025
* __->__ #34024
2025-07-29 10:53:13 -07:00
Joseph Savona
6891dcb87d [compiler] treat ref-like identifiers as refs by default (#34005)
`@enableTreatRefLikeIdentifiersAsRefs` is now on by default. I made one
small fix to the render helper logic as part of this, uncovered by
including more tests.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34005).
* #34027
* #34026
* #34025
* #34024
* __->__ #34005
2025-07-29 10:51:10 -07:00
33 changed files with 1299 additions and 30 deletions

View File

@@ -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',
},
],
},
}),
],
]),
],
[

View File

@@ -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':

View File

@@ -111,6 +111,19 @@ export const AliasEffectSchema: z.ZodType<AliasEffectConfig> = z.object({
into: LifetimeIdSchema,
});
export type ImmutableCaptureEffectConfig = {
kind: 'ImmutableCapture';
from: string;
into: string;
};
export const ImmutableCaptureEffectSchema: z.ZodType<ImmutableCaptureEffectConfig> =
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<AliasingEffectConfig> = z.union([
AssignEffectSchema,
AliasEffectSchema,
CaptureEffectSchema,
ImmutableCaptureEffectSchema,
ImpureEffectSchema,
MutateEffectSchema,
MutateTransitiveConditionallySchema,

View File

@@ -27,6 +27,7 @@ import {
eachTerminalOperand,
} from '../HIR/visitors';
import {Err, Ok, Result} from '../Utils/Result';
import {retainWhere} from '../Utils/utils';
/**
* Validates that a function does not access a ref value during render. This includes a partial check
@@ -79,8 +80,18 @@ type RefAccessRefType =
type RefFnType = {readRefEffect: boolean; returnType: RefAccessType};
class Env extends Map<IdentifierId, RefAccessType> {
class Env {
#changed = false;
#data: Map<IdentifierId, RefAccessType> = new Map();
#temporaries: Map<IdentifierId, Place> = new Map();
lookup(place: Place): Place {
return this.#temporaries.get(place.identifier.id) ?? place;
}
define(place: Place, value: Place): void {
this.#temporaries.set(place.identifier.id, value);
}
resetChanged(): void {
this.#changed = false;
@@ -90,8 +101,14 @@ class Env extends Map<IdentifierId, RefAccessType> {
return this.#changed;
}
override set(key: IdentifierId, value: RefAccessType): this {
const cur = this.get(key);
get(key: IdentifierId): RefAccessType | undefined {
const operandId = this.#temporaries.get(key)?.identifier.id ?? key;
return this.#data.get(operandId);
}
set(key: IdentifierId, value: RefAccessType): this {
const operandId = this.#temporaries.get(key)?.identifier.id ?? key;
const cur = this.#data.get(operandId);
const widenedValue = joinRefAccessTypes(value, cur ?? {kind: 'None'});
if (
!(cur == null && widenedValue.kind === 'None') &&
@@ -99,7 +116,8 @@ class Env extends Map<IdentifierId, RefAccessType> {
) {
this.#changed = true;
}
return super.set(key, widenedValue);
this.#data.set(operandId, widenedValue);
return this;
}
}
@@ -107,9 +125,48 @@ export function validateNoRefAccessInRender(
fn: HIRFunction,
): Result<void, CompilerError> {
const env = new Env();
collectTemporariesSidemap(fn, env);
return validateNoRefAccessInRenderImpl(fn, env).map(_ => undefined);
}
function collectTemporariesSidemap(fn: HIRFunction, env: Env): void {
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {lvalue, value} = instr;
switch (value.kind) {
case 'LoadLocal': {
const temp = env.lookup(value.place);
if (temp != null) {
env.define(lvalue, temp);
}
break;
}
case 'StoreLocal': {
const temp = env.lookup(value.value);
if (temp != null) {
env.define(lvalue, temp);
env.define(value.lvalue.place, temp);
}
break;
}
case 'PropertyLoad': {
if (
isUseRefType(value.object.identifier) &&
value.property === 'current'
) {
continue;
}
const temp = env.lookup(value.object);
if (temp != null) {
env.define(lvalue, temp);
}
break;
}
}
}
}
}
function refTypeOfType(place: Place): RefAccessType {
if (isRefValueType(place.identifier)) {
return {kind: 'RefValue'};
@@ -279,9 +336,10 @@ function validateNoRefAccessInRenderImpl(
for (let i = 0; (i == 0 || env.hasChanged()) && i < 10; i++) {
env.resetChanged();
returnValues = [];
const safeBlocks = new Map<BlockId, RefId>();
const safeBlocks: Array<{block: BlockId; ref: RefId}> = [];
const errors = new CompilerError();
for (const [, block] of fn.body.blocks) {
retainWhere(safeBlocks, entry => entry.block !== block.id);
for (const phi of block.phis) {
env.set(
phi.place.identifier.id,
@@ -432,7 +490,12 @@ function validateNoRefAccessInRenderImpl(
* By default we check that function call operands are not refs,
* ref values, or functions that can access refs.
*/
if (isRefLValue || hookKind != null) {
if (
isRefLValue ||
(hookKind != null &&
hookKind !== 'useState' &&
hookKind !== 'useReducer')
) {
/**
* Special cases:
*
@@ -503,23 +566,39 @@ function validateNoRefAccessInRenderImpl(
case 'PropertyStore':
case 'ComputedDelete':
case 'ComputedStore': {
const safe = safeBlocks.get(block.id);
const target = env.get(instr.value.object.identifier.id);
let safe: (typeof safeBlocks)['0'] | null | undefined = null;
if (
instr.value.kind === 'PropertyStore' &&
safe != null &&
target?.kind === 'Ref' &&
target.refId === safe
target != null &&
target.kind === 'Ref'
) {
safeBlocks.delete(block.id);
safe = safeBlocks.find(entry => entry.ref === target.refId);
}
if (safe != null) {
retainWhere(safeBlocks, entry => entry !== safe);
} else {
validateNoRefUpdate(errors, env, instr.value.object, instr.loc);
}
for (const operand of eachInstructionValueOperand(instr.value)) {
if (operand === instr.value.object) {
continue;
if (
instr.value.kind === 'ComputedDelete' ||
instr.value.kind === 'ComputedStore'
) {
validateNoRefValueAccess(errors, env, instr.value.property);
}
if (
instr.value.kind === 'ComputedStore' ||
instr.value.kind === 'PropertyStore'
) {
validateNoDirectRefValueAccess(errors, instr.value.value, env);
const type = env.get(instr.value.value.identifier.id);
if (type != null && type.kind === 'Structure') {
let objectType: RefAccessType = type;
if (target != null) {
objectType = joinRefAccessTypes(objectType, target);
}
env.set(instr.value.object.identifier.id, objectType);
}
validateNoRefValueAccess(errors, env, operand);
}
break;
}
@@ -599,8 +678,11 @@ function validateNoRefAccessInRenderImpl(
if (block.terminal.kind === 'if') {
const test = env.get(block.terminal.test.identifier.id);
if (test?.kind === 'Guard') {
safeBlocks.set(block.terminal.consequent, test.refId);
if (
test?.kind === 'Guard' &&
safeBlocks.find(entry => entry.ref === test.refId) == null
) {
safeBlocks.push({block: block.terminal.fallthrough, ref: test.refId});
}
}
@@ -718,11 +800,7 @@ function validateNoRefUpdate(
loc: SourceLocation,
): void {
const type = destructure(env.get(operand.identifier.id));
if (
type?.kind === 'Ref' ||
type?.kind === 'RefValue' ||
(type?.kind === 'Structure' && type.fn?.readRefEffect)
) {
if (type?.kind === 'Ref' || type?.kind === 'RefValue') {
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,

View File

@@ -0,0 +1,52 @@
## Input
```javascript
import {useRef} from 'react';
import {Stringify} from 'shared-runtime';
function Component(props) {
const ref = useRef(props.value);
const object = {};
object.foo = () => ref.current;
return <Stringify object={object} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useRef } from "react";
import { Stringify } from "shared-runtime";
function Component(props) {
const $ = _c(1);
const ref = useRef(props.value);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const object = {};
object.foo = () => ref.current;
t0 = <Stringify object={object} shouldInvokeFns={true} />;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 42 }],
};
```
### Eval output
(kind: ok) <div>{"object":{"foo":{"kind":"Function","result":42}},"shouldInvokeFns":true}</div>

View File

@@ -0,0 +1,14 @@
import {useRef} from 'react';
import {Stringify} from 'shared-runtime';
function Component(props) {
const ref = useRef(props.value);
const object = {};
object.foo = () => ref.current;
return <Stringify object={object} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};

View File

@@ -0,0 +1,68 @@
## Input
```javascript
// @validateRefAccessDuringRender
import {useRef} from 'react';
function Component(props) {
const ref = useRef(null);
if (ref.current == null) {
// the logical means the ref write is in a different block
// from the if consequent. this tests that the "safe" blocks
// extend up to the if's fallthrough
ref.current = props.unknownKey ?? props.value;
}
return <Child ref={ref} />;
}
function Child({ref}) {
'use no memo';
return ref.current;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender
import { useRef } from "react";
function Component(props) {
const $ = _c(1);
const ref = useRef(null);
if (ref.current == null) {
ref.current = props.unknownKey ?? props.value;
}
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <Child ref={ref} />;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
function Child({ ref }) {
"use no memo";
return ref.current;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 42 }],
};
```
### Eval output
(kind: ok) 42

View File

@@ -0,0 +1,24 @@
// @validateRefAccessDuringRender
import {useRef} from 'react';
function Component(props) {
const ref = useRef(null);
if (ref.current == null) {
// the logical means the ref write is in a different block
// from the if consequent. this tests that the "safe" blocks
// extend up to the if's fallthrough
ref.current = props.unknownKey ?? props.value;
}
return <Child ref={ref} />;
}
function Child({ref}) {
'use no memo';
return ref.current;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};

View File

@@ -0,0 +1,45 @@
## Input
```javascript
import {useReducer, useRef} from 'react';
function Component(props) {
const ref = useRef(props.value);
const [state] = useReducer(
(state, action) => state + action,
0,
init => ref.current
);
return <Stringify state={state} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};
```
## Error
```
Found 1 error:
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef)
error.invalid-access-ref-in-reducer-init.ts:8:4
6 | (state, action) => state + action,
7 | 0,
> 8 | init => ref.current
| ^^^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render
9 | );
10 |
11 | return <Stringify state={state} />;
```

View File

@@ -0,0 +1,17 @@
import {useReducer, useRef} from 'react';
function Component(props) {
const ref = useRef(props.value);
const [state] = useReducer(
(state, action) => state + action,
0,
init => ref.current
);
return <Stringify state={state} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};

View File

@@ -0,0 +1,41 @@
## Input
```javascript
import {useReducer, useRef} from 'react';
function Component(props) {
const ref = useRef(props.value);
const [state] = useReducer(() => ref.current, null);
return <Stringify state={state} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};
```
## Error
```
Found 1 error:
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef)
error.invalid-access-ref-in-reducer.ts:5:29
3 | function Component(props) {
4 | const ref = useRef(props.value);
> 5 | const [state] = useReducer(() => ref.current, null);
| ^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render
6 |
7 | return <Stringify state={state} />;
8 | }
```

View File

@@ -0,0 +1,13 @@
import {useReducer, useRef} from 'react';
function Component(props) {
const ref = useRef(props.value);
const [state] = useReducer(() => ref.current, null);
return <Stringify state={state} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};

View File

@@ -0,0 +1,37 @@
## Input
```javascript
import {useRef} from 'react';
function Component() {
const ref = useRef(null);
const object = {};
object.foo = () => ref.current;
const refValue = object.foo();
return <div>{refValue}</div>;
}
```
## Error
```
Found 1 error:
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef)
error.invalid-access-ref-in-render-mutate-object-with-ref-function.ts:7:19
5 | const object = {};
6 | object.foo = () => ref.current;
> 7 | const refValue = object.foo();
| ^^^^^^^^^^ This function accesses a ref value
8 | return <div>{refValue}</div>;
9 | }
10 |
```

View File

@@ -0,0 +1,9 @@
import {useRef} from 'react';
function Component() {
const ref = useRef(null);
const object = {};
object.foo = () => ref.current;
const refValue = object.foo();
return <div>{refValue}</div>;
}

View File

@@ -0,0 +1,41 @@
## Input
```javascript
import {useRef, useState} from 'react';
function Component(props) {
const ref = useRef(props.value);
const [state] = useState(() => ref.current);
return <Stringify state={state} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};
```
## Error
```
Found 1 error:
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef)
error.invalid-access-ref-in-state-initializer.ts:5:27
3 | function Component(props) {
4 | const ref = useRef(props.value);
> 5 | const [state] = useState(() => ref.current);
| ^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render
6 |
7 | return <Stringify state={state} />;
8 | }
```

View File

@@ -0,0 +1,13 @@
import {useRef, useState} from 'react';
function Component(props) {
const ref = useRef(props.value);
const [state] = useState(() => ref.current);
return <Stringify state={state} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
};

View File

@@ -41,14 +41,13 @@ Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef)
error.invalid-use-ref-added-to-dep-without-type-info.ts:10:21
8 | // however, this is an instance of accessing a ref during render and is disallowed
9 | // under React's rules, so we reject this input
> 10 | const x = {a, val: val.ref.current};
| ^^^^^^^^^^^^^^^ Cannot access ref value during render
error.invalid-use-ref-added-to-dep-without-type-info.ts:12:28
10 | const x = {a, val: val.ref.current};
11 |
12 | return <VideoList videos={x} />;
> 12 | return <VideoList videos={x} />;
| ^ Cannot access ref value during render
13 | }
14 |
```

View File

@@ -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 <Stringify entries={entries} />;
}
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 | });
```

View File

@@ -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 <Stringify entries={entries} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{object: {key: makeObject_Primitives()}}],
};

View File

@@ -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 <Stringify values={values} />;
}
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 | });
```

View File

@@ -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 <Stringify values={values} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{object: {key: makeObject_Primitives()}}],
};

View File

@@ -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 <Stringify entries={entries} />;
}
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 = <Stringify entries={entries} />;
$[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) <div>{"entries":[["object",{"key":{"a":0,"b":"value1","c":true},"updated":true}]]}</div>

View File

@@ -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 <Stringify entries={entries} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{object: {key: makeObject_Primitives()}}],
};

View File

@@ -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 => <Stringify ref={ref} {...item} />])
),
[items]
);
// Without a declaration for Object.entries(), this would be assumed to mutate
// `record`, meaning existing memoization couldn't be preserved
return (
<div>
{Object.keys(record).map(id => (
<Stringify key={id} render={record[id]} />
))}
</div>
);
}
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 = (
<div>
{t2.map((id) => (
<Stringify key={id} render={record[id]} />
))}
</div>
);
$[4] = record;
$[5] = t2;
$[6] = t3;
} else {
t3 = $[6];
}
return t3;
}
function _temp(item) {
return [item.id, (ref) => <Stringify ref={ref} {...item} />];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [
{
items: [
{ id: "0", name: "Hello" },
{ id: "1", name: "World!" },
],
},
],
};
```
### Eval output
(kind: ok) <div><div>{"render":"[[ function params=1 ]]"}</div><div>{"render":"[[ function params=1 ]]"}</div></div>

View File

@@ -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 => <Stringify ref={ref} {...item} />])
),
[items]
);
// Without a declaration for Object.entries(), this would be assumed to mutate
// `record`, meaning existing memoization couldn't be preserved
return (
<div>
{Object.keys(record).map(id => (
<Stringify key={id} render={record[id]} />
))}
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [
{
items: [
{id: '0', name: 'Hello'},
{id: '1', name: 'World!'},
],
},
],
};

View File

@@ -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 <Stringify entries={entries} />;
}
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 = <Stringify entries={entries} />;
$[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) <div>{"entries":[["object",{"key":{"a":0,"b":"value1","c":true},"updated":true}]]}</div>

View File

@@ -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 <Stringify entries={entries} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{object: {key: makeObject_Primitives()}}],
};

View File

@@ -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 => <Stringify ref={ref} {...item} />},
])
),
[items]
);
// Without a declaration for Object.entries(), this would be assumed to mutate
// `record`, meaning existing memoization couldn't be preserved
return (
<div>
{Object.values(record).map(({id, render}) => (
<Stringify key={id} render={render} />
))}
</div>
);
}
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 = <div>{Object.values(record).map(_temp2)}</div>;
$[2] = record;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
function _temp2(t0) {
const { id, render } = t0;
return <Stringify key={id} render={render} />;
}
function _temp(item) {
return [
item.id,
{ id: item.id, render: (ref) => <Stringify ref={ref} {...item} /> },
];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [
{
items: [
{ id: "0", name: "Hello" },
{ id: "1", name: "World!" },
],
},
],
};
```
### Eval output
(kind: ok) <div><div>{"render":"[[ function params=1 ]]"}</div><div>{"render":"[[ function params=1 ]]"}</div></div>

View File

@@ -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 => <Stringify ref={ref} {...item} />},
])
),
[items]
);
// Without a declaration for Object.entries(), this would be assumed to mutate
// `record`, meaning existing memoization couldn't be preserved
return (
<div>
{Object.values(record).map(({id, render}) => (
<Stringify key={id} render={render} />
))}
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [
{
items: [
{id: '0', name: 'Hello'},
{id: '1', name: 'World!'},
],
},
],
};

View File

@@ -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 => <Stringify ref={ref} {...item} />])
),
[items]
);
// Without a declaration for Object.entries(), this would be assumed to mutate
// `record`, meaning existing memoization couldn't be preserved
return (
<div>
{Object.entries(record).map(([id, render]) => (
<Stringify key={id} render={render} />
))}
</div>
);
}
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 = <div>{Object.entries(record).map(_temp2)}</div>;
$[2] = record;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
function _temp2(t0) {
const [id, render] = t0;
return <Stringify key={id} render={render} />;
}
function _temp(item) {
return [item.id, (ref) => <Stringify ref={ref} {...item} />];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [
{
items: [
{ id: "0", name: "Hello" },
{ id: "1", name: "World!" },
],
},
],
};
```
### Eval output
(kind: ok) <div><div>{"render":"[[ function params=1 ]]"}</div><div>{"render":"[[ function params=1 ]]"}</div></div>

View File

@@ -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 => <Stringify ref={ref} {...item} />])
),
[items]
);
// Without a declaration for Object.entries(), this would be assumed to mutate
// `record`, meaning existing memoization couldn't be preserved
return (
<div>
{Object.entries(record).map(([id, render]) => (
<Stringify key={id} render={render} />
))}
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [
{
items: [
{id: '0', name: 'Hello'},
{id: '1', name: 'World!'},
],
},
],
};

View File

@@ -101,7 +101,7 @@ const COMPILER_OPTIONS: Partial<PluginOptions> = {
// 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,

View File

@@ -103,7 +103,7 @@ const COMPILER_OPTIONS: Partial<PluginOptions> = {
// 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,