From 79dc706498c4f8ef077167898492693197e1b975 Mon Sep 17 00:00:00 2001
From: Joseph Savona <6425824+josephsavona@users.noreply.github.com>
Date: Tue, 29 Jul 2025 10:03:28 -0700
Subject: [PATCH 1/8] [compiler] Improve ref validation error message (#34003)
Improves the error message for ValidateNoRefAccessInRender, using the
new diagnostic type as well as providing a longer but succinct summary
of what refs are for and why they're unsafe to access in render.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34003).
* #34027
* #34026
* #34025
* #34024
* #34005
* #34006
* #34004
* __->__ #34003
---
.../Validation/ValidateNoRefAccessInRender.ts | 180 +++++++++++-------
.../error.capture-ref-for-mutation.expect.md | 34 +---
.../compiler/error.hook-ref-value.expect.md | 12 +-
...invalid-access-ref-during-render.expect.md | 6 +-
...-callback-invoked-during-render-.expect.md | 6 +-
...rrent-inferred-ref-during-render.expect.md | 6 +-
...-disallow-mutating-ref-in-render.expect.md | 6 +-
...tating-refs-in-render-transitive.expect.md | 19 +-
...ror.invalid-pass-ref-to-function.expect.md | 6 +-
...d-ref-prop-in-render-destructure.expect.md | 6 +-
...ref-prop-in-render-property-load.expect.md | 6 +-
...n-callback-invoked-during-render.expect.md | 6 +-
...error.invalid-ref-value-as-props.expect.md | 6 +-
...d-set-and-read-ref-during-render.expect.md | 12 +-
...ef-nested-property-during-render.expect.md | 12 +-
...f-added-to-dep-without-type-info.expect.md | 12 +-
...rite-but-dont-read-ref-in-render.expect.md | 6 +-
...invalid-write-ref-prop-in-render.expect.md | 6 +-
...ror.ref-initialization-arbitrary.expect.md | 12 +-
.../error.ref-initialization-call-2.expect.md | 6 +-
.../error.ref-initialization-call.expect.md | 6 +-
.../error.ref-initialization-linear.expect.md | 6 +-
.../error.ref-initialization-nonif.expect.md | 12 +-
.../error.ref-initialization-other.expect.md | 6 +-
...ref-initialization-post-access-2.expect.md | 6 +-
...r.ref-initialization-post-access.expect.md | 6 +-
.../compiler/error.ref-optional.expect.md | 6 +-
.../error.repro-ref-mutable-range.expect.md | 6 +-
...ified-later-preserve-memoization.expect.md | 6 +-
...ia-function-preserve-memoization.expect.md | 19 +-
...operty-dont-preserve-memoization.expect.md | 6 +-
...alidate-mutate-ref-arg-in-render.expect.md | 6 +-
....maybe-mutable-ref-not-preserved.expect.md | 6 +-
.../error.useMemo-with-refs.flow.expect.md | 6 +-
34 files changed, 273 insertions(+), 195 deletions(-)
diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
index d00302559b..cb78fc1d87 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
@@ -5,7 +5,11 @@
* LICENSE file in the root directory of this source tree.
*/
-import {CompilerError, ErrorSeverity} from '../CompilerError';
+import {
+ CompilerDiagnostic,
+ CompilerError,
+ ErrorSeverity,
+} from '../CompilerError';
import {
BlockId,
HIRFunction,
@@ -385,28 +389,40 @@ function validateNoRefAccessInRenderImpl(
const hookKind = getHookKindForType(fn.env, callee.identifier.type);
let returnType: RefAccessType = {kind: 'None'};
const fnType = env.get(callee.identifier.id);
+ let didError = false;
if (fnType?.kind === 'Structure' && fnType.fn !== null) {
returnType = fnType.fn.returnType;
if (fnType.fn.readRefEffect) {
- errors.push({
- severity: ErrorSeverity.InvalidReact,
- reason:
- 'This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)',
- loc: callee.loc,
- description:
- callee.identifier.name !== null &&
- callee.identifier.name.kind === 'named'
- ? `Function \`${callee.identifier.name.value}\` accesses a ref`
- : null,
- suggestions: null,
- });
+ didError = true;
+ errors.pushDiagnostic(
+ CompilerDiagnostic.create({
+ severity: ErrorSeverity.InvalidReact,
+ category: 'Cannot access refs during render',
+ description: ERROR_DESCRIPTION,
+ }).withDetail({
+ kind: 'error',
+ loc: callee.loc,
+ message: `This function accesses a ref value`,
+ }),
+ );
}
}
- for (const operand of eachInstructionValueOperand(instr.value)) {
- if (hookKind != null) {
- validateNoDirectRefValueAccess(errors, operand, env);
- } else {
- validateNoRefAccess(errors, env, operand, operand.loc);
+ if (!didError) {
+ /*
+ * If we already reported an error on this instruction, don't report
+ * duplicate errors
+ */
+ for (const operand of eachInstructionValueOperand(instr.value)) {
+ if (hookKind != null) {
+ validateNoDirectRefValueAccess(errors, operand, env);
+ } else {
+ validateNoRefPassedToFunction(
+ errors,
+ env,
+ operand,
+ operand.loc,
+ );
+ }
}
}
env.set(instr.lvalue.identifier.id, returnType);
@@ -449,7 +465,7 @@ function validateNoRefAccessInRenderImpl(
) {
safeBlocks.delete(block.id);
} else {
- validateNoRefAccess(errors, env, instr.value.object, instr.loc);
+ validateNoRefUpdate(errors, env, instr.value.object, instr.loc);
}
for (const operand of eachInstructionValueOperand(instr.value)) {
if (operand === instr.value.object) {
@@ -583,18 +599,17 @@ function destructure(
function guardCheck(errors: CompilerError, operand: Place, env: Env): void {
if (env.get(operand.identifier.id)?.kind === 'Guard') {
- errors.push({
- severity: ErrorSeverity.InvalidReact,
- reason:
- 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
- loc: operand.loc,
- description:
- operand.identifier.name !== null &&
- operand.identifier.name.kind === 'named'
- ? `Cannot access ref value \`${operand.identifier.name.value}\``
- : null,
- suggestions: null,
- });
+ errors.pushDiagnostic(
+ CompilerDiagnostic.create({
+ severity: ErrorSeverity.InvalidReact,
+ category: 'Cannot access refs during render',
+ description: ERROR_DESCRIPTION,
+ }).withDetail({
+ kind: 'error',
+ loc: operand.loc,
+ message: `Cannot access ref value during render`,
+ }),
+ );
}
}
@@ -608,22 +623,21 @@ function validateNoRefValueAccess(
type?.kind === 'RefValue' ||
(type?.kind === 'Structure' && type.fn?.readRefEffect)
) {
- errors.push({
- severity: ErrorSeverity.InvalidReact,
- reason:
- 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
- loc: (type.kind === 'RefValue' && type.loc) || operand.loc,
- description:
- operand.identifier.name !== null &&
- operand.identifier.name.kind === 'named'
- ? `Cannot access ref value \`${operand.identifier.name.value}\``
- : null,
- suggestions: null,
- });
+ errors.pushDiagnostic(
+ CompilerDiagnostic.create({
+ severity: ErrorSeverity.InvalidReact,
+ category: 'Cannot access refs during render',
+ description: ERROR_DESCRIPTION,
+ }).withDetail({
+ kind: 'error',
+ loc: (type.kind === 'RefValue' && type.loc) || operand.loc,
+ message: `Cannot access ref value during render`,
+ }),
+ );
}
}
-function validateNoRefAccess(
+function validateNoRefPassedToFunction(
errors: CompilerError,
env: Env,
operand: Place,
@@ -635,18 +649,43 @@ function validateNoRefAccess(
type?.kind === 'RefValue' ||
(type?.kind === 'Structure' && type.fn?.readRefEffect)
) {
- errors.push({
- severity: ErrorSeverity.InvalidReact,
- reason:
- 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
- loc: (type.kind === 'RefValue' && type.loc) || loc,
- description:
- operand.identifier.name !== null &&
- operand.identifier.name.kind === 'named'
- ? `Cannot access ref value \`${operand.identifier.name.value}\``
- : null,
- suggestions: null,
- });
+ errors.pushDiagnostic(
+ CompilerDiagnostic.create({
+ severity: ErrorSeverity.InvalidReact,
+ category: 'Cannot access refs during render',
+ description: ERROR_DESCRIPTION,
+ }).withDetail({
+ kind: 'error',
+ loc: (type.kind === 'RefValue' && type.loc) || loc,
+ message: `Passing a ref to a function may read its value during render`,
+ }),
+ );
+ }
+}
+
+function validateNoRefUpdate(
+ errors: CompilerError,
+ env: Env,
+ operand: Place,
+ loc: SourceLocation,
+): void {
+ const type = destructure(env.get(operand.identifier.id));
+ if (
+ type?.kind === 'Ref' ||
+ type?.kind === 'RefValue' ||
+ (type?.kind === 'Structure' && type.fn?.readRefEffect)
+ ) {
+ errors.pushDiagnostic(
+ CompilerDiagnostic.create({
+ severity: ErrorSeverity.InvalidReact,
+ category: 'Cannot access refs during render',
+ description: ERROR_DESCRIPTION,
+ }).withDetail({
+ kind: 'error',
+ loc: (type.kind === 'RefValue' && type.loc) || loc,
+ message: `Cannot update ref during render`,
+ }),
+ );
}
}
@@ -657,17 +696,22 @@ function validateNoDirectRefValueAccess(
): void {
const type = destructure(env.get(operand.identifier.id));
if (type?.kind === 'RefValue') {
- errors.push({
- severity: ErrorSeverity.InvalidReact,
- reason:
- 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
- loc: type.loc ?? operand.loc,
- description:
- operand.identifier.name !== null &&
- operand.identifier.name.kind === 'named'
- ? `Cannot access ref value \`${operand.identifier.name.value}\``
- : null,
- suggestions: null,
- });
+ errors.pushDiagnostic(
+ CompilerDiagnostic.create({
+ severity: ErrorSeverity.InvalidReact,
+ category: 'Cannot access refs during render',
+ description: ERROR_DESCRIPTION,
+ }).withDetail({
+ kind: 'error',
+ loc: type.loc ?? operand.loc,
+ message: `Cannot access ref value during render`,
+ }),
+ );
}
}
+
+const ERROR_DESCRIPTION =
+ '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)';
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-mutation.expect.md
index 36aba1765a..cb2256a187 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-mutation.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-mutation.expect.md
@@ -32,48 +32,30 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
-Found 4 errors:
+Found 2 errors:
-Error: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)
+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.capture-ref-for-mutation.ts:12:13
10 | };
11 | const moveLeft = {
> 12 | handler: handleKey('left')(),
- | ^^^^^^^^^^^^^^^^^ This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^^^^^^^^ This function accesses a ref value
13 | };
14 | const moveRight = {
15 | handler: handleKey('right')(),
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+Error: Cannot access refs during render
-error.capture-ref-for-mutation.ts:12:13
- 10 | };
- 11 | const moveLeft = {
-> 12 | handler: handleKey('left')(),
- | ^^^^^^^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
- 13 | };
- 14 | const moveRight = {
- 15 | handler: handleKey('right')(),
-
-Error: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)
+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.capture-ref-for-mutation.ts:15:13
13 | };
14 | const moveRight = {
> 15 | handler: handleKey('right')(),
- | ^^^^^^^^^^^^^^^^^^ This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)
- 16 | };
- 17 | return [moveLeft, moveRight];
- 18 | }
-
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
-
-error.capture-ref-for-mutation.ts:15:13
- 13 | };
- 14 | const moveRight = {
-> 15 | handler: handleKey('right')(),
- | ^^^^^^^^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^^^^^^^^^ This function accesses a ref value
16 | };
17 | return [moveLeft, moveRight];
18 | }
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.expect.md
index 63c70cb9f9..36949c6504 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.expect.md
@@ -22,24 +22,28 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 2 errors:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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.hook-ref-value.ts:5:23
3 | function Component(props) {
4 | const ref = useRef();
> 5 | useEffect(() => {}, [ref.current]);
- | ^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^^ Cannot access ref value during render
6 | }
7 |
8 | export const FIXTURE_ENTRYPOINT = {
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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.hook-ref-value.ts:5:23
3 | function Component(props) {
4 | const ref = useRef();
> 5 | useEffect(() => {}, [ref.current]);
- | ^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^^ Cannot access ref value during render
6 | }
7 |
8 | export const FIXTURE_ENTRYPOINT = {
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-during-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-during-render.expect.md
index 123428f602..989e68efd8 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-during-render.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-during-render.expect.md
@@ -17,13 +17,15 @@ function Component(props) {
```
Found 1 error:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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-during-render.ts:4:16
2 | function Component(props) {
3 | const ref = useRef(null);
> 4 | const value = ref.current;
- | ^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^^ Cannot access ref value during render
5 | return value;
6 | }
7 |
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md
index 1da271e561..e9be56ad9b 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md
@@ -21,13 +21,15 @@ function Component(props) {
```
Found 1 error:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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-aliased-ref-in-callback-invoked-during-render-.ts:9:33
7 | return ;
8 | };
> 9 | return {props.items.map(item => renderItem(item))};
- | ^^^^^^^^^^^^^^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render
10 | }
11 |
```
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-current-inferred-ref-during-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-current-inferred-ref-during-render.expect.md
index 9c12d955ae..4f4ed63550 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-current-inferred-ref-during-render.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-current-inferred-ref-during-render.expect.md
@@ -20,12 +20,14 @@ component Example() {
```
Found 1 error:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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)
4 | component Example() {
5 | const fooRef = makeObject_Primitives();
> 6 | fooRef.current = true;
- | ^^^^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^^^^^ Cannot update ref during render
7 |
8 | return ;
9 | }
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-ref-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-ref-in-render.expect.md
index 556d9a2637..9f19d10b9d 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-ref-in-render.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-ref-in-render.expect.md
@@ -18,13 +18,15 @@ function Component() {
```
Found 1 error:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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-disallow-mutating-ref-in-render.ts:4:2
2 | function Component() {
3 | const ref = useRef(null);
> 4 | ref.current = false;
- | ^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^^ Cannot update ref during render
5 |
6 | return ;
7 | }
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-refs-in-render-transitive.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-refs-in-render-transitive.expect.md
index dc477ddf4f..740a0519d5 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-refs-in-render-transitive.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-refs-in-render-transitive.expect.md
@@ -21,26 +21,17 @@ function Component() {
## Error
```
-Found 2 errors:
+Found 1 error:
-Error: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)
+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-disallow-mutating-refs-in-render-transitive.ts:9:2
7 | };
8 | const changeRef = setRef;
> 9 | changeRef();
- | ^^^^^^^^^ This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)
- 10 |
- 11 | return ;
- 12 | }
-
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
-
-error.invalid-disallow-mutating-refs-in-render-transitive.ts:9:2
- 7 | };
- 8 | const changeRef = setRef;
-> 9 | changeRef();
- | ^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^ This function accesses a ref value
10 |
11 | return ;
12 | }
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-pass-ref-to-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-pass-ref-to-function.expect.md
index ccf7bbf5cf..79c2a2e4f6 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-pass-ref-to-function.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-pass-ref-to-function.expect.md
@@ -17,13 +17,15 @@ function Component(props) {
```
Found 1 error:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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-pass-ref-to-function.ts:4:16
2 | function Component(props) {
3 | const ref = useRef(null);
> 4 | const x = foo(ref);
- | ^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^ Passing a ref to a function may read its value during render
5 | return x.current;
6 | }
7 |
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-destructure.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-destructure.expect.md
index e21443afec..5521300e29 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-destructure.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-destructure.expect.md
@@ -16,13 +16,15 @@ function Component({ref}) {
```
Found 1 error:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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-read-ref-prop-in-render-destructure.ts:3:16
1 | // @validateRefAccessDuringRender @compilationMode:"infer"
2 | function Component({ref}) {
> 3 | const value = ref.current;
- | ^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^^ Cannot access ref value during render
4 | return
{value}
;
5 | }
6 |
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-property-load.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-property-load.expect.md
index 73963a1bb5..11d95823d4 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-property-load.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-property-load.expect.md
@@ -16,13 +16,15 @@ function Component(props) {
```
Found 1 error:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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-read-ref-prop-in-render-property-load.ts:3:16
1 | // @validateRefAccessDuringRender @compilationMode:"infer"
2 | function Component(props) {
> 3 | const value = props.ref.current;
- | ^^^^^^^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^^^^^^^^ Cannot access ref value during render
4 | return
{value}
;
5 | }
6 |
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-in-callback-invoked-during-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-in-callback-invoked-during-render.expect.md
index 9b34f802e9..6886aa0876 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-in-callback-invoked-during-render.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-in-callback-invoked-during-render.expect.md
@@ -20,13 +20,15 @@ function Component(props) {
```
Found 1 error:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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-ref-in-callback-invoked-during-render.ts:8:33
6 | return ;
7 | };
> 8 | return {props.items.map(item => renderItem(item))};
- | ^^^^^^^^^^^^^^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render
9 | }
10 |
```
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-value-as-props.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-value-as-props.expect.md
index f4535d6c0d..2bbde91d8f 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-value-as-props.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-value-as-props.expect.md
@@ -16,13 +16,15 @@ function Component(props) {
```
Found 1 error:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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-ref-value-as-props.ts:4:19
2 | function Component(props) {
3 | const ref = useRef(null);
> 4 | return ;
- | ^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^^ Cannot access ref value during render
5 | }
6 |
```
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-during-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-during-render.expect.md
index da4daa88e4..296b9f0831 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-during-render.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-during-render.expect.md
@@ -17,24 +17,28 @@ function Component(props) {
```
Found 2 errors:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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-set-and-read-ref-during-render.ts:4:2
2 | function Component(props) {
3 | const ref = useRef(null);
> 4 | ref.current = props.value;
- | ^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^^ Cannot update ref during render
5 | return ref.current;
6 | }
7 |
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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-set-and-read-ref-during-render.ts:5:9
3 | const ref = useRef(null);
4 | ref.current = props.value;
> 5 | return ref.current;
- | ^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^^ Cannot access ref value during render
6 | }
7 |
```
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-nested-property-during-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-nested-property-during-render.expect.md
index 2deb792f4c..ff57f3d171 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-nested-property-during-render.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-nested-property-during-render.expect.md
@@ -17,24 +17,28 @@ function Component(props) {
```
Found 2 errors:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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-set-and-read-ref-nested-property-during-render.ts:4:2
2 | function Component(props) {
3 | const ref = useRef({inner: null});
> 4 | ref.current.inner = props.value;
- | ^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^^ Cannot update ref during render
5 | return ref.current.inner;
6 | }
7 |
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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-set-and-read-ref-nested-property-during-render.ts:5:9
3 | const ref = useRef({inner: null});
4 | ref.current.inner = props.value;
> 5 | return ref.current.inner;
- | ^^^^^^^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^^^^^^^^ Cannot access ref value during render
6 | }
7 |
```
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-use-ref-added-to-dep-without-type-info.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-use-ref-added-to-dep-without-type-info.expect.md
index 60bcd4a9d0..753db32fbd 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-use-ref-added-to-dep-without-type-info.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-use-ref-added-to-dep-without-type-info.expect.md
@@ -24,24 +24,28 @@ function Foo({a}) {
```
Found 2 errors:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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};
- | ^^^^^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^^^^^^ Cannot access ref value during render
11 |
12 | return ;
13 | }
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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};
- | ^^^^^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^^^^^^ Cannot access ref value during render
11 |
12 | return ;
13 | }
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-write-but-dont-read-ref-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-write-but-dont-read-ref-in-render.expect.md
index afdc173440..abce1ed344 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-write-but-dont-read-ref-in-render.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-write-but-dont-read-ref-in-render.expect.md
@@ -19,13 +19,15 @@ function useHook({value}) {
```
Found 1 error:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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-write-but-dont-read-ref-in-render.ts:5:2
3 | const ref = useRef(null);
4 | // Writing to a ref in render is against the rules:
> 5 | ref.current = value;
- | ^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^^ Cannot update ref during render
6 | // returning a ref is allowed, so this alone doesn't trigger an error:
7 | return ref;
8 | }
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-write-ref-prop-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-write-ref-prop-in-render.expect.md
index b0f6c4ab72..0e76607498 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-write-ref-prop-in-render.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-write-ref-prop-in-render.expect.md
@@ -17,13 +17,15 @@ function Component(props) {
```
Found 1 error:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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-write-ref-prop-in-render.ts:4:2
2 | function Component(props) {
3 | const ref = props.ref;
> 4 | ref.current = true;
- | ^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^^ Cannot update ref during render
5 | return
{value}
;
6 | }
7 |
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-arbitrary.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-arbitrary.expect.md
index 2e8a1148a1..2a72559281 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-arbitrary.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-arbitrary.expect.md
@@ -27,22 +27,26 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 2 errors:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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)
6 | component C() {
7 | const r = useRef(DEFAULT_VALUE);
> 8 | if (r.current == DEFAULT_VALUE) {
- | ^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^ Cannot access ref value during render
9 | r.current = 1;
10 | }
11 | }
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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)
7 | const r = useRef(DEFAULT_VALUE);
8 | if (r.current == DEFAULT_VALUE) {
> 9 | r.current = 1;
- | ^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^ Cannot update ref during render
10 | }
11 | }
12 |
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call-2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call-2.expect.md
index 56dbc086ec..f3b292f658 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call-2.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call-2.expect.md
@@ -25,12 +25,14 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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)
5 | const r = useRef(null);
6 | if (r.current == null) {
> 7 | f(r);
- | ^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^ Passing a ref to a function may read its value during render
8 | }
9 | }
10 |
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call.expect.md
index d15e93afef..d57c3ee010 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-call.expect.md
@@ -25,12 +25,14 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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)
5 | const r = useRef(null);
6 | if (r.current == null) {
> 7 | f(r.current);
- | ^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^ Passing a ref to a function may read its value during render
8 | }
9 | }
10 |
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-linear.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-linear.expect.md
index c9d03057ca..211dee52c8 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-linear.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-linear.expect.md
@@ -26,12 +26,14 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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)
6 | if (r.current == null) {
7 | r.current = 42;
> 8 | r.current = 42;
- | ^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^ Cannot update ref during render
9 | }
10 | }
11 |
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-nonif.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-nonif.expect.md
index 6a49eda920..6388f01ee2 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-nonif.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-nonif.expect.md
@@ -26,24 +26,26 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 2 errors:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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)
4 | component C() {
5 | const r = useRef(null);
> 6 | const guard = r.current == null;
- | ^^^^^^^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^^^^^^^^ Cannot access ref value during render
7 | if (guard) {
8 | r.current = 1;
9 | }
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+Error: Cannot access refs during render
-Cannot access ref value `guard`.
+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)
5 | const r = useRef(null);
6 | const guard = r.current == null;
> 7 | if (guard) {
- | ^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^ Cannot access ref value during render
8 | r.current = 1;
9 | }
10 | }
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-other.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-other.expect.md
index e76809425a..4103eaa291 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-other.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-other.expect.md
@@ -26,12 +26,14 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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)
6 | const r2 = useRef(null);
7 | if (r.current == null) {
> 8 | r2.current = 1;
- | ^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^ Cannot update ref during render
9 | }
10 | }
11 |
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access-2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access-2.expect.md
index 541da61879..f04df650ad 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access-2.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access-2.expect.md
@@ -26,12 +26,14 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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)
7 | r.current = 1;
8 | }
> 9 | f(r.current);
- | ^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^ Passing a ref to a function may read its value during render
10 | }
11 |
12 | export const FIXTURE_ENTRYPOINT = {
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access.expect.md
index 2952d92778..b432538f61 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-post-access.expect.md
@@ -26,12 +26,14 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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)
7 | r.current = 1;
8 | }
> 9 | r.current = 1;
- | ^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^ Cannot update ref during render
10 | }
11 |
12 | export const FIXTURE_ENTRYPOINT = {
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-optional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-optional.expect.md
index 9bae9f1241..80609e0338 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-optional.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-optional.expect.md
@@ -22,13 +22,15 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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.ref-optional.ts:5:9
3 | function Component(props) {
4 | const ref = useRef();
> 5 | return ref?.current;
- | ^^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^^^ Cannot access ref value during render
6 | }
7 |
8 | export const FIXTURE_ENTRYPOINT = {
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-ref-mutable-range.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-ref-mutable-range.expect.md
index a0fecdb3aa..9b3f1d9889 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-ref-mutable-range.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-ref-mutable-range.expect.md
@@ -30,13 +30,15 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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.repro-ref-mutable-range.ts:11:36
9 | mutate(value);
10 | if (CONST_TRUE) {
> 11 | return ;
- | ^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^ Passing a ref to a function may read its value during render
12 | }
13 | return value;
14 | }
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.expect.md
index 07ad9b71be..fb472f683b 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.expect.md
@@ -33,13 +33,15 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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.todo-useCallback-set-ref-nested-property-ref-modified-later-preserve-memoization.ts:14:2
12 |
13 | // The ref is modified later, extending its range and preventing memoization of onChange
> 14 | ref.current.inner = null;
- | ^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^^ Cannot update ref during render
15 |
16 | return ;
17 | }
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md
index 4656f7f51b..f93e987565 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md
@@ -34,26 +34,17 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
-Found 2 errors:
+Found 1 error:
-Error: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)
+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.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.ts:17:2
15 | ref.current.inner = null;
16 | };
> 17 | reset();
- | ^^^^^ This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)
- 18 |
- 19 | return ;
- 20 | }
-
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
-
-error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.ts:17:2
- 15 | ref.current.inner = null;
- 16 | };
-> 17 | reset();
- | ^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^ This function accesses a ref value
18 |
19 | return ;
20 | }
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-set-ref-nested-property-dont-preserve-memoization.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-set-ref-nested-property-dont-preserve-memoization.expect.md
index 1c297cfc85..74822389a5 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-set-ref-nested-property-dont-preserve-memoization.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-set-ref-nested-property-dont-preserve-memoization.expect.md
@@ -32,13 +32,15 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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.useCallback-set-ref-nested-property-dont-preserve-memoization.ts:13:2
11 | });
12 |
> 13 | ref.current.inner = null;
- | ^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^^ Cannot update ref during render
14 |
15 | return ;
16 | }
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.validate-mutate-ref-arg-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.validate-mutate-ref-arg-in-render.expect.md
index 293e5b0a6e..06378fe0d0 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.validate-mutate-ref-arg-in-render.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.validate-mutate-ref-arg-in-render.expect.md
@@ -22,13 +22,15 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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.validate-mutate-ref-arg-in-render.ts:3:14
1 | // @validateRefAccessDuringRender:true
2 | function Foo(props, ref) {
> 3 | console.log(ref.current);
- | ^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^^^^^^^^^ Passing a ref to a function may read its value during render
4 | return
{props.bar}
;
5 | }
6 |
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-mutable-ref-not-preserved.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-mutable-ref-not-preserved.expect.md
index 6a26355e15..82bce33951 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-mutable-ref-not-preserved.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-mutable-ref-not-preserved.expect.md
@@ -25,13 +25,15 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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.maybe-mutable-ref-not-preserved.ts:8:33
6 | function useFoo() {
7 | const r = useRef();
> 8 | return useMemo(() => makeArray(r), []);
- | ^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^ Passing a ref to a function may read its value during render
9 | }
10 |
11 | export const FIXTURE_ENTRYPOINT = {
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-with-refs.flow.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-with-refs.flow.expect.md
index 8566ed0cbf..19ffca8af6 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-with-refs.flow.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-with-refs.flow.expect.md
@@ -21,12 +21,14 @@ component Component(disableLocalRef, ref) {
```
Found 1 error:
-Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+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)
5 | const localRef = useFooRef();
6 | const mergedRef = useMemo(() => {
> 7 | return disableLocalRef ? ref : identity(ref, localRef);
- | ^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
+ | ^^^ Passing a ref to a function may read its value during render
8 | }, [disableLocalRef, ref, localRef]);
9 | return ;
10 | }
From 35e58360cbc196d2a3d997d73064062bf8659e40 Mon Sep 17 00:00:00 2001
From: Joe Savona
Date: Tue, 29 Jul 2025 10:05:10 -0700
Subject: [PATCH 2/8] [compiler] Allow mergeRefs pattern (and detect refs
passed as ref prop)
Two related changes:
* ValidateNoRefAccessInRender now allows the mergeRefs pattern, ie a function that aggregates multiple refs into a new ref. This is the main case where we have seen false positive no-ref-in-render errors.
* Behind `@enableTreatRefLikeIdentifiersAsRefs`, we infer values passed as the `ref` prop to some JSX as refs.
The second change is potentially helpful for situations such as
```js
function Component({ref: parentRef}) {
const childRef = useRef(null);
const mergedRef = mergeRefs(parentRef, childRef);
useEffect(() => {
// generally accesses childRef, not mergedRef
}, []);
return ;
}
```
Ie where you create a merged ref but don't access its `.current` property. Without inferring `ref` props as refs, we'd fail to allow this merge refs case.
---
.../src/TypeInference/InferTypes.ts | 12 +++++
.../Validation/ValidateNoRefAccessInRender.ts | 23 +++++++---
.../allow-merge-refs-pattern.expect.md | 44 +++++++++++++++++++
.../compiler/allow-merge-refs-pattern.js | 11 +++++
4 files changed, 85 insertions(+), 5 deletions(-)
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-merge-refs-pattern.expect.md
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-merge-refs-pattern.js
diff --git a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts
index 73088fd852..488d988b97 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts
@@ -451,6 +451,18 @@ function* generateInstructionTypes(
case 'JsxExpression':
case 'JsxFragment': {
+ if (env.config.enableTreatRefLikeIdentifiersAsRefs) {
+ if (value.kind === 'JsxExpression') {
+ for (const prop of value.props) {
+ if (prop.kind === 'JsxAttribute' && prop.name === 'ref') {
+ yield equation(prop.place.identifier.type, {
+ kind: 'Object',
+ shapeId: BuiltInUseRefId,
+ });
+ }
+ }
+ }
+ }
yield equation(left, {kind: 'Object', shapeId: BuiltInJsxId});
break;
}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
index cb78fc1d87..b4fb0d171a 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
@@ -407,15 +407,28 @@ function validateNoRefAccessInRenderImpl(
);
}
}
+ /*
+ * If we already reported an error on this instruction, don't report
+ * duplicate errors
+ */
if (!didError) {
- /*
- * If we already reported an error on this instruction, don't report
- * duplicate errors
- */
+ const isRefLValue = isUseRefType(instr.lvalue.identifier);
for (const operand of eachInstructionValueOperand(instr.value)) {
if (hookKind != null) {
validateNoDirectRefValueAccess(errors, operand, env);
- } else {
+ } else if (!isRefLValue) {
+ /**
+ * In general passing a ref to a function may access that ref
+ * value during render, so we disallow it.
+ *
+ * The main exception is the "mergeRefs" pattern, ie a function
+ * that accepts multiple refs as arguments (or an array of refs)
+ * and returns a new, aggregated ref. If the lvalue is a ref,
+ * we assume that the user is doing this pattern and allow passing
+ * refs.
+ *
+ * Eg `const mergedRef = mergeRefs(ref1, ref2)`
+ */
validateNoRefPassedToFunction(
errors,
env,
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-merge-refs-pattern.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-merge-refs-pattern.expect.md
new file mode 100644
index 0000000000..6933edef46
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-merge-refs-pattern.expect.md
@@ -0,0 +1,44 @@
+
+## Input
+
+```javascript
+// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
+
+import {useRef} from 'react';
+
+function Component() {
+ const ref = useRef(null);
+ const ref2 = useRef(null);
+ const mergedRef = mergeRefs([ref], ref2);
+
+ return ;
+}
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime"; // @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
+
+import { useRef } from "react";
+
+function Component() {
+ const $ = _c(1);
+ const ref = useRef(null);
+ const ref2 = useRef(null);
+ const mergedRef = mergeRefs([ref], ref2);
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = ;
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ return t0;
+}
+
+```
+
+### Eval output
+(kind: exception) Fixture not implemented
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-merge-refs-pattern.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-merge-refs-pattern.js
new file mode 100644
index 0000000000..91c5f08284
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-merge-refs-pattern.js
@@ -0,0 +1,11 @@
+// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
+
+import {useRef} from 'react';
+
+function Component() {
+ const ref = useRef(null);
+ const ref2 = useRef(null);
+ const mergedRef = mergeRefs([ref], ref2);
+
+ return ;
+}
From 21d071e3f383da2f8655aaf07a301fc301e828fb Mon Sep 17 00:00:00 2001
From: Joe Savona
Date: Tue, 29 Jul 2025 10:05:10 -0700
Subject: [PATCH 3/8] [compiler] Allow passing refs to render helpers
We infer render helpers as functions whose result is immediately interpolated into jsx. This is a very conservative approximation, to help with common cases like `{props.renderItem(ref)}`. The idea is similar to hooks that it's ultimately on the developer to catch ref-in-render validations (and the runtime detects them too), so we can be a bit more relaxed since there are valid reasons to use this pattern.
---
.../Validation/ValidateNoRefAccessInRender.ts | 42 ++++++++++++++--
...ef-to-render-helper-props-object.expect.md | 45 +++++++++++++++++
...ssing-ref-to-render-helper-props-object.js | 9 ++++
...low-passing-ref-to-render-helper.expect.md | 49 +++++++++++++++++++
.../allow-passing-ref-to-render-helper.js | 9 ++++
5 files changed, 151 insertions(+), 3 deletions(-)
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper-props-object.expect.md
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper-props-object.js
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper.expect.md
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper.js
diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
index b4fb0d171a..9ef742e415 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
@@ -262,6 +262,20 @@ function validateNoRefAccessInRenderImpl(
env.set(place.identifier.id, type);
}
+ const interpolatedAsJsx = new Set();
+ for (const block of fn.body.blocks.values()) {
+ for (const instr of block.instructions) {
+ const {value} = instr;
+ if (value.kind === 'JsxExpression' || value.kind === 'JsxFragment') {
+ if (value.children != null) {
+ for (const child of value.children) {
+ interpolatedAsJsx.add(child.identifier.id);
+ }
+ }
+ }
+ }
+ }
+
for (let i = 0; (i == 0 || env.hasChanged()) && i < 10; i++) {
env.resetChanged();
returnValues = [];
@@ -414,10 +428,19 @@ function validateNoRefAccessInRenderImpl(
if (!didError) {
const isRefLValue = isUseRefType(instr.lvalue.identifier);
for (const operand of eachInstructionValueOperand(instr.value)) {
- if (hookKind != null) {
- validateNoDirectRefValueAccess(errors, operand, env);
- } else if (!isRefLValue) {
+ /**
+ * By default we check that function call operands are not refs,
+ * ref values, or functions that can access refs.
+ */
+ if (
+ isRefLValue ||
+ interpolatedAsJsx.has(instr.lvalue.identifier.id) ||
+ hookKind != null
+ ) {
/**
+ * Special cases:
+ *
+ * 1) the lvalue is a ref
* In general passing a ref to a function may access that ref
* value during render, so we disallow it.
*
@@ -428,7 +451,20 @@ function validateNoRefAccessInRenderImpl(
* refs.
*
* Eg `const mergedRef = mergeRefs(ref1, ref2)`
+ *
+ * 2) the lvalue is passed as a jsx child
+ *
+ * For example `{renderHelper(ref)}`. Here we have more
+ * context and infer that the ref is being passed to a component-like
+ * render function which attempts to obey the rules.
+ *
+ * 3) hooks
+ *
+ * Hooks are independently checked to ensure they don't access refs
+ * during render.
*/
+ validateNoDirectRefValueAccess(errors, operand, env);
+ } else {
validateNoRefPassedToFunction(
errors,
env,
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper-props-object.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper-props-object.expect.md
new file mode 100644
index 0000000000..f23ab16c16
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper-props-object.expect.md
@@ -0,0 +1,45 @@
+
+## Input
+
+```javascript
+// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
+
+import {useRef} from 'react';
+
+function Component(props) {
+ const ref = useRef(null);
+
+ return {props.render({ref})};
+}
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime"; // @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
+
+import { useRef } from "react";
+
+function Component(props) {
+ const $ = _c(3);
+ const ref = useRef(null);
+
+ const T0 = Foo;
+ const t0 = props.render({ ref });
+ let t1;
+ if ($[0] !== T0 || $[1] !== t0) {
+ t1 = {t0};
+ $[0] = T0;
+ $[1] = t0;
+ $[2] = t1;
+ } else {
+ t1 = $[2];
+ }
+ return t1;
+}
+
+```
+
+### Eval output
+(kind: exception) Fixture not implemented
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper-props-object.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper-props-object.js
new file mode 100644
index 0000000000..ab9ffe2ed3
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper-props-object.js
@@ -0,0 +1,9 @@
+// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
+
+import {useRef} from 'react';
+
+function Component(props) {
+ const ref = useRef(null);
+
+ return {props.render({ref})};
+}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper.expect.md
new file mode 100644
index 0000000000..a0ad22fcaf
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper.expect.md
@@ -0,0 +1,49 @@
+
+## Input
+
+```javascript
+// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
+
+import {useRef} from 'react';
+
+function Component(props) {
+ const ref = useRef(null);
+
+ return {props.render(ref)};
+}
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime"; // @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
+
+import { useRef } from "react";
+
+function Component(props) {
+ const $ = _c(4);
+ const ref = useRef(null);
+ let t0;
+ if ($[0] !== props.render) {
+ t0 = props.render(ref);
+ $[0] = props.render;
+ $[1] = t0;
+ } else {
+ t0 = $[1];
+ }
+ let t1;
+ if ($[2] !== t0) {
+ t1 = {t0};
+ $[2] = t0;
+ $[3] = t1;
+ } else {
+ t1 = $[3];
+ }
+ return t1;
+}
+
+```
+
+### Eval output
+(kind: exception) Fixture not implemented
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper.js
new file mode 100644
index 0000000000..7c5a70188f
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper.js
@@ -0,0 +1,9 @@
+// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
+
+import {useRef} from 'react';
+
+function Component(props) {
+ const ref = useRef(null);
+
+ return {props.render(ref)};
+}
From f482b7639e3450a78305b0462845d4a50a7ac43d Mon Sep 17 00:00:00 2001
From: Joe Savona
Date: Tue, 29 Jul 2025 10:05:10 -0700
Subject: [PATCH 4/8] [compiler] treat ref-like identifiers as refs by default
`@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.
---
.../src/HIR/Environment.ts | 2 +-
.../Validation/ValidateNoRefAccessInRender.ts | 25 +++---
...ow-ref-access-in-effect-indirect.expect.md | 7 +-
.../allow-ref-access-in-effect-indirect.js | 1 +
.../allow-ref-access-in-effect.expect.md | 7 +-
.../compiler/allow-ref-access-in-effect.js | 1 +
...access-in-unused-callback-nested.expect.md | 7 +-
...ow-ref-access-in-unused-callback-nested.js | 1 +
...-callback-invoked-during-render-.expect.md | 2 +-
...n-callback-invoked-during-render.expect.md | 2 +-
.../error.repro-ref-mutable-range.expect.md | 47 ----------
...utate-after-useeffect-ref-access.expect.md | 2 +-
.../nonreactive-ref-helper.expect.md | 4 +-
...utate-after-useeffect-ref-access.expect.md | 2 +-
...nvalid-useCallback-read-maybeRef.expect.md | 39 --------
...be-invalid-useMemo-read-maybeRef.expect.md | 39 --------
...nvalid-useCallback-read-maybeRef.expect.md | 38 ++++++++
...aybe-invalid-useCallback-read-maybeRef.ts} | 0
...be-invalid-useMemo-read-maybeRef.expect.md | 38 ++++++++
...ro-maybe-invalid-useMemo-read-maybeRef.ts} | 0
.../repro-ref-mutable-range.expect.md | 89 +++++++++++++++++++
...-range.tsx => repro-ref-mutable-range.tsx} | 0
22 files changed, 202 insertions(+), 151 deletions(-)
delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-ref-mutable-range.expect.md
delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useCallback-read-maybeRef.expect.md
delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useMemo-read-maybeRef.expect.md
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/repro-maybe-invalid-useCallback-read-maybeRef.expect.md
rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/{error.maybe-invalid-useCallback-read-maybeRef.ts => repro-maybe-invalid-useCallback-read-maybeRef.ts} (100%)
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/repro-maybe-invalid-useMemo-read-maybeRef.expect.md
rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/{error.maybe-invalid-useMemo-read-maybeRef.ts => repro-maybe-invalid-useMemo-read-maybeRef.ts} (100%)
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.expect.md
rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/{error.repro-ref-mutable-range.tsx => repro-ref-mutable-range.tsx} (100%)
diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
index ba7396e0d7..f94870fc03 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
@@ -608,7 +608,7 @@ export const EnvironmentConfigSchema = z.object({
*
* Here the variables `ref` and `myRef` will be typed as Refs.
*/
- enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(false),
+ enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(true),
/*
* If specified a value, the compiler lowers any calls to `useContext` to use
diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
index 9ef742e415..571fe61c81 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
@@ -432,15 +432,11 @@ function validateNoRefAccessInRenderImpl(
* By default we check that function call operands are not refs,
* ref values, or functions that can access refs.
*/
- if (
- isRefLValue ||
- interpolatedAsJsx.has(instr.lvalue.identifier.id) ||
- hookKind != null
- ) {
+ if (isRefLValue || hookKind != null) {
/**
* Special cases:
*
- * 1) the lvalue is a ref
+ * 1. the lvalue is a ref
* In general passing a ref to a function may access that ref
* value during render, so we disallow it.
*
@@ -452,18 +448,21 @@ function validateNoRefAccessInRenderImpl(
*
* Eg `const mergedRef = mergeRefs(ref1, ref2)`
*
- * 2) the lvalue is passed as a jsx child
- *
- * For example `{renderHelper(ref)}`. Here we have more
- * context and infer that the ref is being passed to a component-like
- * render function which attempts to obey the rules.
- *
- * 3) hooks
+ * 2. calling hooks
*
* Hooks are independently checked to ensure they don't access refs
* during render.
*/
validateNoDirectRefValueAccess(errors, operand, env);
+ } else if (interpolatedAsJsx.has(instr.lvalue.identifier.id)) {
+ /**
+ * Special case: the lvalue is passed as a jsx child
+ *
+ * For example `{renderHelper(ref)}`. Here we have more
+ * context and infer that the ref is being passed to a component-like
+ * render function which attempts to obey the rules.
+ */
+ validateNoRefValueAccess(errors, env, operand);
} else {
validateNoRefPassedToFunction(
errors,
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.expect.md
index 7c1f5ad372..6cf97f6c35 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.expect.md
@@ -27,6 +27,7 @@ function Component() {
}
function Child({ref}) {
+ 'use no memo';
// This violates the rules of React, so we access the ref in a child
// component
return ref.current;
@@ -100,8 +101,10 @@ function Component() {
return t6;
}
-function Child(t0) {
- const { ref } = t0;
+function Child({ ref }) {
+ "use no memo";
+ // This violates the rules of React, so we access the ref in a child
+ // component
return ref.current;
}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.js
index 6942904902..4320b5871d 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.js
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.js
@@ -23,6 +23,7 @@ function Component() {
}
function Child({ref}) {
+ 'use no memo';
// This violates the rules of React, so we access the ref in a child
// component
return ref.current;
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.expect.md
index 3bdf358611..009d504da2 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.expect.md
@@ -23,6 +23,7 @@ function Component() {
}
function Child({ref}) {
+ 'use no memo';
// This violates the rules of React, so we access the ref in a child
// component
return ref.current;
@@ -86,8 +87,10 @@ function Component() {
return t5;
}
-function Child(t0) {
- const { ref } = t0;
+function Child({ ref }) {
+ "use no memo";
+ // This violates the rules of React, so we access the ref in a child
+ // component
return ref.current;
}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.js
index efba0547eb..54a1dc22c3 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.js
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.js
@@ -19,6 +19,7 @@ function Component() {
}
function Child({ref}) {
+ 'use no memo';
// This violates the rules of React, so we access the ref in a child
// component
return ref.current;
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.expect.md
index 3584faf699..26e996017e 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.expect.md
@@ -25,6 +25,7 @@ function Component() {
}
function Child({ref}) {
+ 'use no memo';
// This violates the rules of React, so we access the ref in a child
// component
return ref.current;
@@ -83,8 +84,10 @@ function Component() {
}
function _temp() {}
-function Child(t0) {
- const { ref } = t0;
+function Child({ ref }) {
+ "use no memo";
+ // This violates the rules of React, so we access the ref in a child
+ // component
return ref.current;
}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.js
index 1e40288637..dcd8540e2a 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.js
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.js
@@ -21,6 +21,7 @@ function Component() {
}
function Child({ref}) {
+ 'use no memo';
// This violates the rules of React, so we access the ref in a child
// component
return ref.current;
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md
index e9be56ad9b..3aa5237533 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md
@@ -29,7 +29,7 @@ error.invalid-aliased-ref-in-callback-invoked-during-render-.ts:9:33
7 | return ;
8 | };
> 9 | return {props.items.map(item => renderItem(item))};
- | ^^^^^^^^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render
+ | ^^^^^^^^^^^^^^^^^^^^^^^^ Cannot access ref value during render
10 | }
11 |
```
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-in-callback-invoked-during-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-in-callback-invoked-during-render.expect.md
index 6886aa0876..414ee9d536 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-in-callback-invoked-during-render.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-in-callback-invoked-during-render.expect.md
@@ -28,7 +28,7 @@ error.invalid-ref-in-callback-invoked-during-render.ts:8:33
6 | return ;
7 | };
> 8 | return {props.items.map(item => renderItem(item))};
- | ^^^^^^^^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render
+ | ^^^^^^^^^^^^^^^^^^^^^^^^ Cannot access ref value during render
9 | }
10 |
```
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-ref-mutable-range.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-ref-mutable-range.expect.md
deleted file mode 100644
index 9b3f1d9889..0000000000
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-ref-mutable-range.expect.md
+++ /dev/null
@@ -1,47 +0,0 @@
-
-## Input
-
-```javascript
-import {Stringify, identity, mutate, CONST_TRUE} from 'shared-runtime';
-
-function Foo(props, ref) {
- const value = {};
- if (CONST_TRUE) {
- mutate(value);
- return ;
- }
- mutate(value);
- if (CONST_TRUE) {
- return ;
- }
- return value;
-}
-
-export const FIXTURE_ENTRYPOINT = {
- fn: Foo,
- params: [{}, {current: 'fake-ref-object'}],
-};
-
-```
-
-
-## 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.repro-ref-mutable-range.ts:11:36
- 9 | mutate(value);
- 10 | if (CONST_TRUE) {
-> 11 | return ;
- | ^^^ Passing a ref to a function may read its value during render
- 12 | }
- 13 | return value;
- 14 | }
-```
-
-
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect-ref-access.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect-ref-access.expect.md
index 88643995fa..d3b746b1f8 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect-ref-access.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect-ref-access.expect.md
@@ -47,7 +47,7 @@ export const FIXTURE_ENTRYPOINT = {
## Logs
```
-{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":158},"end":{"line":11,"column":1,"index":331},"filename":"mutate-after-useeffect-ref-access.ts"},"detail":{"options":{"severity":"InvalidReact","category":"This value cannot be modified","description":"Modifying component props or hook arguments is not allowed. Consider using a local variable instead.","details":[{"kind":"error","loc":{"start":{"line":9,"column":2,"index":289},"end":{"line":9,"column":16,"index":303},"filename":"mutate-after-useeffect-ref-access.ts"},"message":"value cannot be modified"}]}}}
+{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":158},"end":{"line":11,"column":1,"index":331},"filename":"mutate-after-useeffect-ref-access.ts"},"detail":{"options":{"severity":"InvalidReact","category":"Cannot access refs during render","description":"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)","details":[{"kind":"error","loc":{"start":{"line":9,"column":2,"index":289},"end":{"line":9,"column":16,"index":303},"filename":"mutate-after-useeffect-ref-access.ts"},"message":"Cannot update ref during render"}]}}}
{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":8,"column":2,"index":237},"end":{"line":8,"column":50,"index":285},"filename":"mutate-after-useeffect-ref-access.ts"},"decorations":[{"start":{"line":8,"column":24,"index":259},"end":{"line":8,"column":30,"index":265},"filename":"mutate-after-useeffect-ref-access.ts","identifierName":"arrRef"}]}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":6,"column":0,"index":158},"end":{"line":11,"column":1,"index":331},"filename":"mutate-after-useeffect-ref-access.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/nonreactive-ref-helper.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/nonreactive-ref-helper.expect.md
index ec7ac5e0e3..05dcab7796 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/nonreactive-ref-helper.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/nonreactive-ref-helper.expect.md
@@ -51,12 +51,12 @@ function RefsInEffects() {
const ref = useRefHelper();
const wrapped = useDeeperRefHelper();
let t0;
- if ($[0] !== ref.current || $[1] !== wrapped.foo.current) {
+ if ($[0] !== ref || $[1] !== wrapped.foo.current) {
t0 = () => {
print(ref.current);
print(wrapped.foo.current);
};
- $[0] = ref.current;
+ $[0] = ref;
$[1] = wrapped.foo.current;
$[2] = t0;
} else {
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-ref-access.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-ref-access.expect.md
index 8df9bd9f85..7bcc917447 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-ref-access.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-ref-access.expect.md
@@ -47,7 +47,7 @@ export const FIXTURE_ENTRYPOINT = {
## Logs
```
-{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":190},"end":{"line":11,"column":1,"index":363},"filename":"mutate-after-useeffect-ref-access.ts"},"detail":{"options":{"severity":"InvalidReact","category":"This value cannot be modified","description":"Modifying component props or hook arguments is not allowed. Consider using a local variable instead.","details":[{"kind":"error","loc":{"start":{"line":9,"column":2,"index":321},"end":{"line":9,"column":16,"index":335},"filename":"mutate-after-useeffect-ref-access.ts"},"message":"value cannot be modified"}]}}}
+{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":190},"end":{"line":11,"column":1,"index":363},"filename":"mutate-after-useeffect-ref-access.ts"},"detail":{"options":{"severity":"InvalidReact","category":"Cannot access refs during render","description":"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)","details":[{"kind":"error","loc":{"start":{"line":9,"column":2,"index":321},"end":{"line":9,"column":16,"index":335},"filename":"mutate-after-useeffect-ref-access.ts"},"message":"Cannot update ref during render"}]}}}
{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":8,"column":2,"index":269},"end":{"line":8,"column":50,"index":317},"filename":"mutate-after-useeffect-ref-access.ts"},"decorations":[{"start":{"line":8,"column":24,"index":291},"end":{"line":8,"column":30,"index":297},"filename":"mutate-after-useeffect-ref-access.ts","identifierName":"arrRef"}]}
{"kind":"CompileSuccess","fnLoc":{"start":{"line":6,"column":0,"index":190},"end":{"line":11,"column":1,"index":363},"filename":"mutate-after-useeffect-ref-access.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
```
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useCallback-read-maybeRef.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useCallback-read-maybeRef.expect.md
deleted file mode 100644
index 8cc7c5a49d..0000000000
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useCallback-read-maybeRef.expect.md
+++ /dev/null
@@ -1,39 +0,0 @@
-
-## Input
-
-```javascript
-// @validatePreserveExistingMemoizationGuarantees
-import {useCallback} from 'react';
-
-function useHook(maybeRef) {
- return useCallback(() => {
- return [maybeRef.current];
- }, [maybeRef]);
-}
-
-```
-
-
-## Error
-
-```
-Found 1 error:
-
-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. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `maybeRef.current`, but the source dependencies were [maybeRef]. Differences in ref.current access.
-
-error.maybe-invalid-useCallback-read-maybeRef.ts:5:21
- 3 |
- 4 | function useHook(maybeRef) {
-> 5 | return useCallback(() => {
- | ^^^^^^^
-> 6 | return [maybeRef.current];
- | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-> 7 | }, [maybeRef]);
- | ^^^^ Could not preserve existing manual memoization
- 8 | }
- 9 |
-```
-
-
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useMemo-read-maybeRef.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useMemo-read-maybeRef.expect.md
deleted file mode 100644
index 38fd74e66a..0000000000
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useMemo-read-maybeRef.expect.md
+++ /dev/null
@@ -1,39 +0,0 @@
-
-## Input
-
-```javascript
-// @validatePreserveExistingMemoizationGuarantees
-import {useMemo} from 'react';
-
-function useHook(maybeRef, shouldRead) {
- return useMemo(() => {
- return () => [maybeRef.current];
- }, [shouldRead, maybeRef]);
-}
-
-```
-
-
-## Error
-
-```
-Found 1 error:
-
-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. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `maybeRef.current`, but the source dependencies were [shouldRead, maybeRef]. Differences in ref.current access.
-
-error.maybe-invalid-useMemo-read-maybeRef.ts:5:17
- 3 |
- 4 | function useHook(maybeRef, shouldRead) {
-> 5 | return useMemo(() => {
- | ^^^^^^^
-> 6 | return () => [maybeRef.current];
- | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-> 7 | }, [shouldRead, maybeRef]);
- | ^^^^ Could not preserve existing manual memoization
- 8 | }
- 9 |
-```
-
-
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/repro-maybe-invalid-useCallback-read-maybeRef.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/repro-maybe-invalid-useCallback-read-maybeRef.expect.md
new file mode 100644
index 0000000000..0ccfd1e43a
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/repro-maybe-invalid-useCallback-read-maybeRef.expect.md
@@ -0,0 +1,38 @@
+
+## Input
+
+```javascript
+// @validatePreserveExistingMemoizationGuarantees
+import {useCallback} from 'react';
+
+function useHook(maybeRef) {
+ return useCallback(() => {
+ return [maybeRef.current];
+ }, [maybeRef]);
+}
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
+import { useCallback } from "react";
+
+function useHook(maybeRef) {
+ const $ = _c(2);
+ let t0;
+ if ($[0] !== maybeRef) {
+ t0 = () => [maybeRef.current];
+ $[0] = maybeRef;
+ $[1] = t0;
+ } else {
+ t0 = $[1];
+ }
+ return t0;
+}
+
+```
+
+### Eval output
+(kind: exception) Fixture not implemented
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useCallback-read-maybeRef.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/repro-maybe-invalid-useCallback-read-maybeRef.ts
similarity index 100%
rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useCallback-read-maybeRef.ts
rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/repro-maybe-invalid-useCallback-read-maybeRef.ts
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/repro-maybe-invalid-useMemo-read-maybeRef.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/repro-maybe-invalid-useMemo-read-maybeRef.expect.md
new file mode 100644
index 0000000000..9d02bff8b4
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/repro-maybe-invalid-useMemo-read-maybeRef.expect.md
@@ -0,0 +1,38 @@
+
+## Input
+
+```javascript
+// @validatePreserveExistingMemoizationGuarantees
+import {useMemo} from 'react';
+
+function useHook(maybeRef, shouldRead) {
+ return useMemo(() => {
+ return () => [maybeRef.current];
+ }, [shouldRead, maybeRef]);
+}
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
+import { useMemo } from "react";
+
+function useHook(maybeRef, shouldRead) {
+ const $ = _c(2);
+ let t0;
+ if ($[0] !== maybeRef) {
+ t0 = () => [maybeRef.current];
+ $[0] = maybeRef;
+ $[1] = t0;
+ } else {
+ t0 = $[1];
+ }
+ return t0;
+}
+
+```
+
+### Eval output
+(kind: exception) Fixture not implemented
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useMemo-read-maybeRef.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/repro-maybe-invalid-useMemo-read-maybeRef.ts
similarity index 100%
rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useMemo-read-maybeRef.ts
rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/repro-maybe-invalid-useMemo-read-maybeRef.ts
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.expect.md
new file mode 100644
index 0000000000..0ce46d3cc5
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.expect.md
@@ -0,0 +1,89 @@
+
+## Input
+
+```javascript
+import {Stringify, identity, mutate, CONST_TRUE} from 'shared-runtime';
+
+function Foo(props, ref) {
+ const value = {};
+ if (CONST_TRUE) {
+ mutate(value);
+ return ;
+ }
+ mutate(value);
+ if (CONST_TRUE) {
+ return ;
+ }
+ return value;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Foo,
+ params: [{}, {current: 'fake-ref-object'}],
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime";
+import { Stringify, identity, mutate, CONST_TRUE } from "shared-runtime";
+
+function Foo(props, ref) {
+ const $ = _c(7);
+ let t0;
+ let value;
+ if ($[0] !== ref) {
+ t0 = Symbol.for("react.early_return_sentinel");
+ bb0: {
+ value = {};
+ if (CONST_TRUE) {
+ mutate(value);
+ t0 = ;
+ break bb0;
+ }
+
+ mutate(value);
+ }
+ $[0] = ref;
+ $[1] = t0;
+ $[2] = value;
+ } else {
+ t0 = $[1];
+ value = $[2];
+ }
+ if (t0 !== Symbol.for("react.early_return_sentinel")) {
+ return t0;
+ }
+ if (CONST_TRUE) {
+ let t1;
+ if ($[3] !== ref) {
+ t1 = identity(ref);
+ $[3] = ref;
+ $[4] = t1;
+ } else {
+ t1 = $[4];
+ }
+ let t2;
+ if ($[5] !== t1) {
+ t2 = ;
+ $[5] = t1;
+ $[6] = t2;
+ } else {
+ t2 = $[6];
+ }
+ return t2;
+ }
+ return value;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Foo,
+ params: [{}, { current: "fake-ref-object" }],
+};
+
+```
+
+### Eval output
+(kind: ok)
{"ref":{"current":"fake-ref-object"}}
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-ref-mutable-range.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.tsx
similarity index 100%
rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-ref-mutable-range.tsx
rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.tsx
From b383925576bafb46f1ac6cd68b815d997f43d4e9 Mon Sep 17 00:00:00 2001
From: Joe Savona
Date: Tue, 29 Jul 2025 10:05:10 -0700
Subject: [PATCH 5/8] [compiler] ref guards apply up to fallthrough of the test
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.
---
.../Validation/ValidateNoRefAccessInRender.ts | 23 ++++---
...lazy-initialization-with-logical.expect.md | 68 +++++++++++++++++++
...ow-ref-lazy-initialization-with-logical.js | 24 +++++++
3 files changed, 107 insertions(+), 8 deletions(-)
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.expect.md
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.js
diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
index 571fe61c81..70aa4eeea8 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
@@ -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
@@ -279,9 +280,10 @@ function validateNoRefAccessInRenderImpl(
for (let i = 0; (i == 0 || env.hasChanged()) && i < 10; i++) {
env.resetChanged();
returnValues = [];
- const safeBlocks = new Map();
+ 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,
@@ -503,15 +505,17 @@ 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);
}
@@ -599,8 +603,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});
}
}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.expect.md
new file mode 100644
index 0000000000..3540e842f6
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.expect.md
@@ -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 ;
+}
+
+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 = ;
+ $[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
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.js
new file mode 100644
index 0000000000..2e1b03a28d
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.js
@@ -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 ;
+}
+
+function Child({ref}) {
+ 'use no memo';
+ return ref.current;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{value: 42}],
+};
From 966897a25440efc4c47d489eb0b9f2cd7655db6a Mon Sep 17 00:00:00 2001
From: Joe Savona
Date: Tue, 29 Jul 2025 10:05:10 -0700
Subject: [PATCH 6/8] [compiler] disallow ref access in state initializer,
reducer/initializer
Per title, disallow ref access in `useState()` initializer function, `useReducer()` reducer, and `useReducer()` init function.
---
.../Validation/ValidateNoRefAccessInRender.ts | 7 ++-
...valid-access-ref-in-reducer-init.expect.md | 45 +++++++++++++++++++
...rror.invalid-access-ref-in-reducer-init.js | 17 +++++++
...or.invalid-access-ref-in-reducer.expect.md | 41 +++++++++++++++++
.../error.invalid-access-ref-in-reducer.js | 13 ++++++
...-access-ref-in-state-initializer.expect.md | 41 +++++++++++++++++
...invalid-access-ref-in-state-initializer.js | 13 ++++++
7 files changed, 176 insertions(+), 1 deletion(-)
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.expect.md
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.js
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.expect.md
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.js
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.expect.md
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.js
diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
index 70aa4eeea8..c10d1bc07e 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
@@ -434,7 +434,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:
*
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.expect.md
new file mode 100644
index 0000000000..29fe24a220
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.expect.md
@@ -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 ;
+}
+
+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 ;
+```
+
+
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.js
new file mode 100644
index 0000000000..df10b8a9eb
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.js
@@ -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 ;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{value: 42}],
+};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.expect.md
new file mode 100644
index 0000000000..f23560b4f6
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.expect.md
@@ -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 ;
+}
+
+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 ;
+ 8 | }
+```
+
+
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.js
new file mode 100644
index 0000000000..135a78e0ba
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.js
@@ -0,0 +1,13 @@
+import {useReducer, useRef} from 'react';
+
+function Component(props) {
+ const ref = useRef(props.value);
+ const [state] = useReducer(() => ref.current, null);
+
+ return ;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{value: 42}],
+};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.expect.md
new file mode 100644
index 0000000000..dd6a64d9db
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.expect.md
@@ -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 ;
+}
+
+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 ;
+ 8 | }
+```
+
+
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.js
new file mode 100644
index 0000000000..c3f233023e
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.js
@@ -0,0 +1,13 @@
+import {useRef, useState} from 'react';
+
+function Component(props) {
+ const ref = useRef(props.value);
+ const [state] = useState(() => ref.current);
+
+ return ;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{value: 42}],
+};
From 8eca2e657b469ac310a714fde208c7342ae99812 Mon Sep 17 00:00:00 2001
From: Joe Savona
Date: Tue, 29 Jul 2025 10:05:10 -0700
Subject: [PATCH 7/8] [compiler] Allow assigning ref-accessing functions to
objects if not mutated
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.
---
.../Validation/ValidateNoRefAccessInRender.ts | 92 ++++++++++++++++---
...o-object-property-if-not-mutated.expect.md | 52 +++++++++++
...ction-to-object-property-if-not-mutated.js | 14 +++
...-mutate-object-with-ref-function.expect.md | 37 ++++++++
...-render-mutate-object-with-ref-function.js | 9 ++
...f-added-to-dep-without-type-info.expect.md | 11 +--
6 files changed, 196 insertions(+), 19 deletions(-)
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.expect.md
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.js
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.expect.md
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.js
diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
index c10d1bc07e..e1c17625f4 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
@@ -80,8 +80,18 @@ type RefAccessRefType =
type RefFnType = {readRefEffect: boolean; returnType: RefAccessType};
-class Env extends Map {
+class Env {
#changed = false;
+ #data: Map = new Map();
+ #temporaries: Map = 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;
@@ -91,8 +101,14 @@ class Env extends Map {
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') &&
@@ -100,7 +116,8 @@ class Env extends Map {
) {
this.#changed = true;
}
- return super.set(key, widenedValue);
+ this.#data.set(operandId, widenedValue);
+ return this;
}
}
@@ -108,9 +125,48 @@ export function validateNoRefAccessInRender(
fn: HIRFunction,
): Result {
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'};
@@ -524,11 +580,25 @@ function validateNoRefAccessInRenderImpl(
} 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;
}
@@ -730,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,
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.expect.md
new file mode 100644
index 0000000000..b5fc0a9dc7
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.expect.md
@@ -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 ;
+}
+
+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 = ;
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ return t0;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{ value: 42 }],
+};
+
+```
+
+### Eval output
+(kind: ok)
;
+}
+
+```
+
+
+## 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
{refValue}
;
+ 9 | }
+ 10 |
+```
+
+
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.js
new file mode 100644
index 0000000000..9d3faac764
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.js
@@ -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
{refValue}
;
+}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-use-ref-added-to-dep-without-type-info.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-use-ref-added-to-dep-without-type-info.expect.md
index 753db32fbd..f41ae64ce7 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-use-ref-added-to-dep-without-type-info.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-use-ref-added-to-dep-without-type-info.expect.md
@@ -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 ;
+> 12 | return ;
+ | ^ Cannot access ref value during render
13 | }
+ 14 |
```
\ No newline at end of file
From 1b4f28e24db13bbdfbe7edd789c39706e1ec9af6 Mon Sep 17 00:00:00 2001
From: Joe Savona
Date: Tue, 29 Jul 2025 10:05:10 -0700
Subject: [PATCH 8/8] [compiler] Detect known incompatible libraries
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
A few libraries are known to be incompatible with memoization, whether manually via `useMemo()` or via React Compiler. This puts us in a tricky situation. On the one hand, we understand that these libraries were developed prior to our documenting the [Rules of React](https://react.dev/reference/rules), and their designs were the result of trying to deliver a great experience for their users and balance multiple priorities around DX, performance, etc. At the same time, using these libraries with memoization — and in particular with automatic memoization via React Compiler — can break apps by causing the components using these APIs not to update. Concretely, the APIs have in common that they return a function which returns different values over time, but where the function itself does not change. Memoizing the result on the identity of the function will mean that the value never changes. Developers reasonable interpret this as "React Compiler broke my code".
Of course, the best solution is to work with developers of these libraries to address the root cause, and we're doing that. We've previously discussed this situation with both of the respective libraries:
* React Hook Form: https://github.com/react-hook-form/react-hook-form/issues/11910#issuecomment-2135608761
* TanStack Table: https://github.com/facebook/react/issues/33057#issuecomment-2840600158 and https://github.com/TanStack/table/issues/5567
In the meantime we need to make sure that React Compiler can work out of the box as much as possible. This means teaching it about popular libraries that cannot be memoized. We also can't silently skip compilation, as this confuses users, so we need these error messages to be visible to users. To that end, this PR adds:
* A flag to mark functions/hooks as incompatible
* Validation against use of such functions
* A default type provider to provide declarations for two known-incompatible libraries
Note that Mobx is also incompatible, but the `observable()` function is called outside of the component itself, so the compiler cannot currently detect it. We may add validation for such APIs in the future.
Again, we really empathize with the developers of these libraries. We've tried to word the error message non-judgementally, because we get that it's hard! We're open to feedback about the error message, please let us know.
---
.../src/HIR/DefaultModuleTypeProvider.ts | 81 +++++++++++++++++++
.../src/HIR/Environment.ts | 5 +-
.../src/HIR/Globals.ts | 2 +
.../src/HIR/ObjectShape.ts | 1 +
.../src/HIR/TypeSchema.ts | 4 +
.../Inference/InferMutationAliasingEffects.ts | 20 +++++
...alid-known-incompatible-function.expect.md | 34 ++++++++
...ror.invalid-known-incompatible-function.js | 6 ++
...ncompatible-hook-return-property.expect.md | 33 ++++++++
...known-incompatible-hook-return-property.js | 6 ++
....invalid-known-incompatible-hook.expect.md | 34 ++++++++
.../error.invalid-known-incompatible-hook.js | 6 ++
.../sprout/shared-runtime-type-provider.ts | 45 +++++++++++
13 files changed, 276 insertions(+), 1 deletion(-)
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/HIR/DefaultModuleTypeProvider.ts
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-function.expect.md
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-function.js
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook-return-property.expect.md
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook-return-property.js
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook.expect.md
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook.js
diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/DefaultModuleTypeProvider.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/DefaultModuleTypeProvider.ts
new file mode 100644
index 0000000000..0cd65d947f
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/DefaultModuleTypeProvider.ts
@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import {Effect, ValueKind} from '..';
+import {TypeConfig} from './TypeSchema';
+
+/**
+ * Libraries developed before we officially documented the [Rules of React](https://react.dev/reference/rules)
+ * implement APIs which cannot be memoized safely, either via manual or automatic memoization.
+ *
+ * Any non-hook API that is designed to be called during render (not events/effects) should be safe to memoize:
+ *
+ * ```js
+ * function Component() {
+ * const {someFunction} = useLibrary();
+ * // it should always be safe to memoize functions like this
+ * const result = useMemo(() => someFunction(), [someFunction]);
+ * }
+ * ```
+ *
+ * However, some APIs implement "interior mutability" — mutating values rather than copying into a new value
+ * and setting state with the new value — which defaults such memoization. With this pattern, the function
+ * (`someFunction()` in the example) could return different values even though the function itself is the same.
+ *
+ * Given that we didn't have the Rules of React precisely documented prior to the introduction of React compiler,
+ * it's understandable that some libraries accidentally shipped APIs that break this rule. However, developers
+ * can easily run into pitfalls with these APIs. They may manually memoize them, which can break their app. Or
+ * they may try using React Compiler, and think that the compiler has broken their code.
+ *
+ * The React team is open to collaborating with library authors to help develop compatible versions of these APIs,
+ * and we have already reached out to the teams who own any API listed here to ensure they are aware of the issue.
+ */
+export function defaultModuleTypeProvider(
+ moduleName: string,
+): TypeConfig | null {
+ switch (moduleName) {
+ case 'react-hook-form': {
+ return {
+ kind: 'object',
+ properties: {
+ useForm: {
+ kind: 'hook',
+ returnType: {
+ kind: 'object',
+ properties: {
+ watch: {
+ kind: 'function',
+ positionalParams: [],
+ restParam: Effect.Read,
+ calleeEffect: Effect.Read,
+ returnType: {kind: 'type', name: 'Any'},
+ returnValueKind: ValueKind.Mutable,
+ knownIncompatible: `React Hook Form's \`useForm()\` API returns a \`watch()\` function which cannot be memoized safely.`,
+ },
+ },
+ },
+ },
+ },
+ };
+ }
+ case '@tanstack/react-table': {
+ return {
+ kind: 'object',
+ properties: {
+ useReactTable: {
+ kind: 'hook',
+ positionalParams: [],
+ restParam: Effect.Read,
+ returnType: {kind: 'type', name: 'Any'},
+ knownIncompatible: `TanStack Table's \`useReactTable()\` API returns functions that cannot be memoized safely`,
+ },
+ },
+ };
+ }
+ }
+ return null;
+}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
index f94870fc03..80ea2180fb 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
@@ -49,6 +49,7 @@ import {
} from './ObjectShape';
import {Scope as BabelScope, NodePath} from '@babel/traverse';
import {TypeSchema} from './TypeSchema';
+import {defaultModuleTypeProvider} from './DefaultModuleTypeProvider';
export const ReactElementSymbolSchema = z.object({
elementSymbol: z.union([
@@ -157,7 +158,9 @@ export const EnvironmentConfigSchema = z.object({
* A function that, given the name of a module, can optionally return a description
* of that module's type signature.
*/
- moduleTypeProvider: z.nullable(z.function().args(z.string())).default(null),
+ moduleTypeProvider: z
+ .nullable(z.function().args(z.string()))
+ .default(defaultModuleTypeProvider),
/**
* A list of functions which the application compiles as macros, where
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 c3eadb89f5..89d1529180 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts
@@ -908,6 +908,7 @@ export function installTypeConfig(
mutableOnlyIfOperandsAreMutable:
typeConfig.mutableOnlyIfOperandsAreMutable === true,
aliasing: typeConfig.aliasing,
+ knownIncompatible: typeConfig.knownIncompatible ?? null,
});
}
case 'hook': {
@@ -926,6 +927,7 @@ export function installTypeConfig(
returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen,
noAlias: typeConfig.noAlias === true,
aliasing: typeConfig.aliasing,
+ knownIncompatible: typeConfig.knownIncompatible ?? null,
});
}
case 'object': {
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 eaf728db95..b20bebbae3 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts
@@ -331,6 +331,7 @@ export type FunctionSignature = {
mutableOnlyIfOperandsAreMutable?: boolean;
impure?: boolean;
+ knownIncompatible?: string | null | undefined;
canonicalName?: string;
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 5945e3a078..8f28a1357b 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts
@@ -236,6 +236,7 @@ export type FunctionTypeConfig = {
impure?: boolean | null | undefined;
canonicalName?: string | null | undefined;
aliasing?: AliasingSignatureConfig | null | undefined;
+ knownIncompatible?: string | null | undefined;
};
export const FunctionTypeSchema: z.ZodType = z.object({
kind: z.literal('function'),
@@ -249,6 +250,7 @@ export const FunctionTypeSchema: z.ZodType = z.object({
impure: z.boolean().nullable().optional(),
canonicalName: z.string().nullable().optional(),
aliasing: AliasingSignatureSchema.nullable().optional(),
+ knownIncompatible: z.string().nullable().optional(),
});
export type HookTypeConfig = {
@@ -259,6 +261,7 @@ export type HookTypeConfig = {
returnValueKind?: ValueKind | null | undefined;
noAlias?: boolean | null | undefined;
aliasing?: AliasingSignatureConfig | null | undefined;
+ knownIncompatible?: string | null | undefined;
};
export const HookTypeSchema: z.ZodType = z.object({
kind: z.literal('hook'),
@@ -268,6 +271,7 @@ export const HookTypeSchema: z.ZodType = z.object({
returnValueKind: ValueKindSchema.nullable().optional(),
noAlias: z.boolean().nullable().optional(),
aliasing: AliasingSignatureSchema.nullable().optional(),
+ knownIncompatible: z.string().nullable().optional(),
});
export type BuiltInTypeConfig =
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 2adf78fe05..0edde82db9 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts
@@ -2120,6 +2120,26 @@ function computeEffectsForLegacySignature(
}),
});
}
+ if (signature.knownIncompatible != null) {
+ const errors = new CompilerError();
+ errors.pushDiagnostic(
+ CompilerDiagnostic.create({
+ severity: ErrorSeverity.InvalidReact,
+ category: 'Use of incompatible library',
+ description: [
+ 'This API returns functions which cannot be memoized without leading to stale UI. ' +
+ 'To prevent this, by default React Compiler will skip memoizing this component/hook. ' +
+ 'However, you may see issues if values from this API are passed to other components/hooks that are ' +
+ 'memoized.',
+ ].join(''),
+ }).withDetail({
+ kind: 'error',
+ loc: receiver.loc,
+ message: signature.knownIncompatible,
+ }),
+ );
+ throw errors;
+ }
const stores: Array = [];
const captures: Array = [];
function visit(place: Place, effect: Effect): void {
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-function.expect.md
new file mode 100644
index 0000000000..fc1afa7b66
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-function.expect.md
@@ -0,0 +1,34 @@
+
+## Input
+
+```javascript
+import {knownIncompatible} from 'ReactCompilerKnownIncompatibleTest';
+
+function Component() {
+ const data = knownIncompatible();
+ return
Error
;
+}
+
+```
+
+
+## Error
+
+```
+Found 1 error:
+
+Error: Use of incompatible library
+
+This API returns functions which cannot be memoized without leading to stale UI. To prevent this, by default React Compiler will skip memoizing this component/hook. However, you may see issues if values from this API are passed to other components/hooks that are memoized.
+
+error.invalid-known-incompatible-function.ts:4:15
+ 2 |
+ 3 | function Component() {
+> 4 | const data = knownIncompatible();
+ | ^^^^^^^^^^^^^^^^^ useKnownIncompatible is known to be incompatible
+ 5 | return
Error
;
+ 6 | }
+ 7 |
+```
+
+
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-function.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-function.js
new file mode 100644
index 0000000000..778b6dd045
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-function.js
@@ -0,0 +1,6 @@
+import {knownIncompatible} from 'ReactCompilerKnownIncompatibleTest';
+
+function Component() {
+ const data = knownIncompatible();
+ return
;
+}
+
+```
+
+
+## Error
+
+```
+Found 1 error:
+
+Error: Use of incompatible library
+
+This API returns functions which cannot be memoized without leading to stale UI. To prevent this, by default React Compiler will skip memoizing this component/hook. However, you may see issues if values from this API are passed to other components/hooks that are memoized.
+
+error.invalid-known-incompatible-hook-return-property.ts:5:15
+ 3 | function Component() {
+ 4 | const {incompatible} = useKnownIncompatibleIndirect();
+> 5 | return
{incompatible()}
;
+ | ^^^^^^^^^^^^ useKnownIncompatibleIndirect returns an incompatible() function that is known incompatible
+ 6 | }
+ 7 |
+```
+
+
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook-return-property.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook-return-property.js
new file mode 100644
index 0000000000..1160ccb4dc
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook-return-property.js
@@ -0,0 +1,6 @@
+import {useKnownIncompatibleIndirect} from 'ReactCompilerKnownIncompatibleTest';
+
+function Component() {
+ const {incompatible} = useKnownIncompatibleIndirect();
+ return
{incompatible()}
;
+}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook.expect.md
new file mode 100644
index 0000000000..4a2e85581c
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook.expect.md
@@ -0,0 +1,34 @@
+
+## Input
+
+```javascript
+import {useKnownIncompatible} from 'ReactCompilerKnownIncompatibleTest';
+
+function Component() {
+ const data = useKnownIncompatible();
+ return
Error
;
+}
+
+```
+
+
+## Error
+
+```
+Found 1 error:
+
+Error: Use of incompatible library
+
+This API returns functions which cannot be memoized without leading to stale UI. To prevent this, by default React Compiler will skip memoizing this component/hook. However, you may see issues if values from this API are passed to other components/hooks that are memoized.
+
+error.invalid-known-incompatible-hook.ts:4:15
+ 2 |
+ 3 | function Component() {
+> 4 | const data = useKnownIncompatible();
+ | ^^^^^^^^^^^^^^^^^^^^ useKnownIncompatible is known to be incompatible
+ 5 | return
Error
;
+ 6 | }
+ 7 |
+```
+
+
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook.js
new file mode 100644
index 0000000000..618516c55c
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-known-incompatible-hook.js
@@ -0,0 +1,6 @@
+import {useKnownIncompatible} from 'ReactCompilerKnownIncompatibleTest';
+
+function Component() {
+ const data = useKnownIncompatible();
+ return
Error
;
+}
diff --git a/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts b/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts
index 58b007c1c7..b01a204e78 100644
--- a/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts
+++ b/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts
@@ -198,6 +198,51 @@ export function makeSharedRuntimeTypeProvider({
},
},
};
+ } else if (moduleName === 'ReactCompilerKnownIncompatibleTest') {
+ /**
+ * Fake module used for testing validation of known incompatible
+ * API validation
+ */
+ return {
+ kind: 'object',
+ properties: {
+ useKnownIncompatible: {
+ kind: 'hook',
+ positionalParams: [],
+ restParam: EffectEnum.Read,
+ returnType: {kind: 'type', name: 'Any'},
+ knownIncompatible: `useKnownIncompatible is known to be incompatible`,
+ },
+ useKnownIncompatibleIndirect: {
+ kind: 'hook',
+ positionalParams: [],
+ restParam: EffectEnum.Read,
+ returnType: {
+ kind: 'object',
+ properties: {
+ incompatible: {
+ kind: 'function',
+ positionalParams: [],
+ restParam: EffectEnum.Read,
+ calleeEffect: EffectEnum.Read,
+ returnType: {kind: 'type', name: 'Any'},
+ returnValueKind: ValueKindEnum.Mutable,
+ knownIncompatible: `useKnownIncompatibleIndirect returns an incompatible() function that is known incompatible`,
+ },
+ },
+ },
+ },
+ knownIncompatible: {
+ kind: 'function',
+ positionalParams: [],
+ restParam: EffectEnum.Read,
+ calleeEffect: EffectEnum.Read,
+ returnType: {kind: 'type', name: 'Any'},
+ returnValueKind: ValueKindEnum.Mutable,
+ knownIncompatible: `useKnownIncompatible is known to be incompatible`,
+ },
+ },
+ };
} else if (moduleName === 'ReactCompilerTest') {
/**
* Fake module used for testing validation that type providers return hook