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 abbb7d847698..541ad0dda4b7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts @@ -655,33 +655,15 @@ function validateNoRefAccessInRenderImpl( if (instr.value.operator === '!') { const value = env.get(instr.value.value.identifier.id); const refId = - value?.kind === 'RefValue' && value.refId != null - ? value.refId + value?.kind === 'RefValue' + ? (value.refId ?? nextRefId()) : null; if (refId !== null) { /* - * Record an error suggesting the `if (ref.current == null)` pattern, - * but also record the lvalue as a guard so that we don't emit a second - * error for the write to the ref + * Allow !ref.current patterns for ref initialization as they are + * semantically equivalent to ref.current == null checks */ env.set(instr.lvalue.identifier.id, {kind: 'Guard', refId}); - errors.pushDiagnostic( - CompilerDiagnostic.create({ - category: ErrorCategory.Refs, - reason: 'Cannot access refs during render', - description: ERROR_DESCRIPTION, - }) - .withDetails({ - kind: 'error', - loc: instr.value.value.loc, - message: `Cannot access ref value during render`, - }) - .withDetails({ - kind: 'hint', - message: - 'To initialize a ref only once, check that the ref is null with the pattern `if (ref.current == null) { ref.current = ... }`', - }), - ); break; } } @@ -693,18 +675,21 @@ function validateNoRefAccessInRenderImpl( const right = env.get(instr.value.right.identifier.id); let nullish: boolean = false; let refId: RefId | null = null; - if (left?.kind === 'RefValue' && left.refId != null) { - refId = left.refId; - } else if (right?.kind === 'RefValue' && right.refId != null) { - refId = right.refId; + + if (left?.kind === 'RefValue') { + refId = left.refId ?? nextRefId(); + } else if (right?.kind === 'RefValue') { + refId = right.refId ?? nextRefId(); } + // Check for null or undefined values if (left?.kind === 'Nullable') { nullish = true; } else if (right?.kind === 'Nullable') { nullish = true; } + // Allow ref comparisons with null/undefined as guards if (refId !== null && nullish) { env.set(instr.lvalue.identifier.id, {kind: 'Guard', refId}); } else { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-access-render-unary.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-access-render-unary.expect.md index ce1be800a13b..8093488bb3d3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-access-render-unary.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-access-render-unary.expect.md @@ -22,21 +22,7 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` -Found 4 errors: - -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 current = !r.current; - | ^^^^^^^^^ Cannot access ref value during render - 7 | return
{current}
; - 8 | } - 9 | - -To initialize a ref only once, check that the ref is null with the pattern `if (ref.current == null) { ref.current = ... }` +Found 3 errors: Error: Cannot access refs during render diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-initialization-unary-not.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-initialization-unary-not.expect.md deleted file mode 100644 index 516d006c205a..000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-initialization-unary-not.expect.md +++ /dev/null @@ -1,43 +0,0 @@ - -## Input - -```javascript -//@flow -import {useRef} from 'react'; - -component C() { - const r = useRef(null); - if (!r.current) { - r.current = 1; - } -} - -export const FIXTURE_ENTRYPOINT = { - fn: C, - params: [{}], -}; - -``` - - -## 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). - - 4 | component C() { - 5 | const r = useRef(null); -> 6 | if (!r.current) { - | ^^^^^^^^^ Cannot access ref value during render - 7 | r.current = 1; - 8 | } - 9 | } - -To initialize a ref only once, check that the ref is null with the pattern `if (ref.current == null) { ref.current = ... }` -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-undefined-initialization-negation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-undefined-initialization-negation.expect.md new file mode 100644 index 000000000000..2ea91e8e87af --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-undefined-initialization-negation.expect.md @@ -0,0 +1,51 @@ + +## Input + +```javascript +import {useRef} from 'react'; + +function Component() { + const ref = useRef(undefined); + if (!ref.current) { + ref.current = "initialized"; + } + return
Hello World
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +}; +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useRef } from "react"; + +function Component() { + const $ = _c(1); + const ref = useRef(undefined); + if (!ref.current) { + ref.current = "initialized"; + } + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 =
Hello World
; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +}; + +``` + +### Eval output +(kind: ok)
Hello World
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-undefined-initialization-negation.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-undefined-initialization-negation.js new file mode 100644 index 000000000000..49cb0da33dc4 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-undefined-initialization-negation.js @@ -0,0 +1,14 @@ +import {useRef} from 'react'; + +function Component() { + const ref = useRef(undefined); + if (!ref.current) { + ref.current = 'initialized'; + } + return
Hello World
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-undefined-initialization.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-undefined-initialization.expect.md new file mode 100644 index 000000000000..f9f870449bab --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-undefined-initialization.expect.md @@ -0,0 +1,51 @@ + +## Input + +```javascript +import {useRef} from 'react'; + +function Component() { + const ref = useRef(undefined); + if (ref.current === undefined) { + ref.current = "initialized"; + } + return
Hello World
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +}; +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useRef } from "react"; + +function Component() { + const $ = _c(1); + const ref = useRef(undefined); + if (ref.current === undefined) { + ref.current = "initialized"; + } + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 =
Hello World
; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +}; + +``` + +### Eval output +(kind: ok)
Hello World
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-undefined-initialization.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-undefined-initialization.js new file mode 100644 index 000000000000..bf7844ad81be --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-undefined-initialization.js @@ -0,0 +1,14 @@ +import {useRef} from 'react'; + +function Component() { + const ref = useRef(undefined); + if (ref.current === undefined) { + ref.current = 'initialized'; + } + return
Hello World
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-ref-initialization-unary-not.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-ref-initialization-unary-not.expect.md new file mode 100644 index 000000000000..4e8d9c4b57a0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-ref-initialization-unary-not.expect.md @@ -0,0 +1,42 @@ + +## Input + +```javascript +//@flow +import {useRef} from 'react'; + +component C() { + const r = useRef(null); + if (!r.current) { + r.current = 1; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; + +``` + +## Code + +```javascript +import { useRef } from "react"; + +function C() { + const r = useRef(null); + if (!r.current) { + r.current = 1; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; + +``` + +### Eval output +(kind: ok) \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-initialization-unary-not.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-ref-initialization-unary-not.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-initialization-unary-not.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-ref-initialization-unary-not.js