-
Notifications
You must be signed in to change notification settings - Fork 50.4k
[compiler] Improve impurity/ref validation #35298
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
josephsavona
wants to merge
1
commit into
main
Choose a base branch
from
pr35298
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
josephsavona
commented
Dec 5, 2025
josephsavona
commented
Dec 5, 2025
josephsavona
commented
Dec 5, 2025
compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts
Outdated
Show resolved
Hide resolved
josephsavona
commented
Dec 5, 2025
compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts
Outdated
Show resolved
Hide resolved
josephsavona
commented
Dec 5, 2025
.../src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render-indirect.expect.md
Outdated
Show resolved
Hide resolved
josephsavona
commented
Dec 5, 2025
...lugin-react-compiler/src/__tests__/fixtures/compiler/valid-use-impure-value-in-ref.expect.md
Outdated
Show resolved
Hide resolved
f0e4d14 to
2cd48b2
Compare
3ccb2f9 to
9899f74
Compare
d4c7683 to
66bce76
Compare
This was referenced Jan 16, 2026
4c35df6 to
28f77b4
Compare
2976f25 to
91a6cc3
Compare
# Summary note: This implements the idea discussed in reactwg/react#389 (comment) This is a large PR that significantly changes our impurity and ref validation to address multiple issues. The goal is to reduce false positives and make the errors we do report more actionable. ## Validating Against Impure Values In Render Currently we create `Impure` effects for impure functions like `Date.now()` or `Math.random()`, and then throw if the effect is reachable during render. However, impurity is a property of the resulting value: if the value isn't accessed during render then it's okay: maybe you're console-logging the time while debugging (fine), or storing the impure value into a ref and only accessing it in an effect or event handler (totally ok). This PR updates to validate that impure values are not transitively consumed during render, building on the new effects system: rather than look at instruction types, we use effects like `Capture a -> b`, `Impure a`, and `Render b` to determine how impure values are introduced and where they flow through the program. We're intentionally conservative, and do _not_ propagate impurity through MaybeCapture effects, which is used for functions we don't have signatures for. This means that things like `identity(performance.now())` drop the impurity. This feels like a good compromise since it means we have very high confidence in the errors that we report and can always add increased strictness later as our confidence increases. An example error: ``` Error: Cannot access impure value during render Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). error.invalid-impure-value-in-render-helper.ts:5:17 3 | const now = () => Date.now(); 4 | const render = () => { > 5 | return <div>{now()}</div>; | ^^^^^ Cannot access impure value during render 6 | }; 7 | return <div>{render()}</div>; 8 | } error.invalid-impure-value-in-render-helper.ts:3:20 1 | // @validateNoImpureFunctionsInRender 2 | function Component() { > 3 | const now = () => Date.now(); | ^^^^^^^^^^ `Date.now` is an impure function. 4 | const render = () => { 5 | return <div>{now()}</div>; 6 | }; ``` Impure values are allowed to flow into refs, meaning that we now allow `useRef(Date.now())` or `useRef(localFunctionThatReturnsMathDotRandom())` which would have errored previously. The next PR reuses this improved impurity tracking to validate ref access in render as well. ## Refs Now Treated As Impure Values in Render Reading a ref now produces an `Impure` effect, and reading refs in render is validated using the above validation against impure values in render. The error category and message is customized for refs, we're just reusing the validation implementation. This means you get consistent results for `performance.now()` as for `ref.current`. A nice consistency win. ## Simplified writing-ref validation Now that _reading_ a ref in render is validated using the impure values infra, I also dramatically simplified ValidateNoRefAccessInRender to focus solely on validation against _writing_ refs during render. It was harder to use the new effects infra for this since we intentionally do not record ref mutations as a `Mutate` effect. So for now, the pass switches on InstructionValue variants. We continue to support the `if (ref.current == null) { ref.current = <init> }` pattern and reasonable variants of it. We're conservative about what we consider to be a write of a ref - `foo(ref)` now assumes you're not mutating rather than the inverse. # Takeaways * Impure-values-in-render logic is more conservative about what it considers an error (ie to users it appears more persmissive). We follow clearly identifiable (and if we wanted traceable/explainable) paths from impure sources through to where they are rendered. We allow many more cases than before, notably `x = foo(ref)` optimistically assumes you don't read the ref. Concretely, this follows from not tracking impure values across MaybeCapture effects. * No-writing-refs-in-render is also more conservative about what it counts as a write, appearing to users as allowing more cases. We look for very direct evidence of writing to refs, ie `ref.current = <value>` or `ref.current.property = <value>`, and allow potential writes through eg `writeToRef(ref, value)`.
josephsavona
commented
Jan 16, 2026
Comment on lines
-23
to
+43
| Error: Cannot access refs during render | ||
| Error: Cannot access ref value 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 <Foo item={item} current={current} />; | ||
| 7 | }; | ||
| > 8 | return <Items>{props.items.map(item => renderItem(item))}</Items>; | ||
| | ^^^^^^^^^^^^^^^^^^^^^^^^ Cannot access ref value during render | ||
| 9 | } | ||
| 10 | | ||
| error.invalid-ref-in-callback-invoked-during-render.ts:6:37 | ||
| 4 | const renderItem = item => { | ||
| 5 | const current = ref.current; | ||
| > 6 | return <Foo item={item} current={current} />; | ||
| | ^^^^^^^ Ref value is used during render | ||
| 7 | }; | ||
| 8 | return <Items>{props.items.map(item => renderItem(item))}</Items>; | ||
| 9 | } | ||
| error.invalid-ref-in-callback-invoked-during-render.ts:5:20 | ||
| 3 | const ref = useRef(null); | ||
| 4 | const renderItem = item => { | ||
| > 5 | const current = ref.current; | ||
| | ^^^^^^^^^^^ Ref is initially accessed | ||
| 6 | return <Foo item={item} current={current} />; | ||
| 7 | }; | ||
| 8 | return <Items>{props.items.map(item => renderItem(item))}</Items>; |
Member
Author
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This case is a great example of the improvement. The previous error didn't point to anything that obviously had a ref, while the new error points to the source and usage of the ref in JSX.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Summary
note: This implements the idea discussed in https://github.com/reactwg/react/discussions/389#discussioncomment-14252280
This is a large PR that significantly changes our impurity and ref validation to address multiple issues. The goal is to reduce false positives and make the errors we do report more actionable.
Validating Against Impure Values In Render
Currently we create
Impureeffects for impure functions likeDate.now()orMath.random(), and then throw if the effect is reachable during render. However, impurity is a property of the resulting value: if the value isn't accessed during render then it's okay: maybe you're console-logging the time while debugging (fine), or storing the impure value into a ref and only accessing it in an effect or event handler (totally ok).This PR updates to validate that impure values are not transitively consumed during render, building on the new effects system: rather than look at instruction types, we use effects like
Capture a -> b,Impure a, andRender bto determine how impure values are introduced and where they flow through the program. We're intentionally conservative, and do not propagate impurity through MaybeCapture effects, which is used for functions we don't have signatures for. This means that things likeidentity(performance.now())drop the impurity. This feels like a good compromise since it means we have very high confidence in the errors that we report and can always add increased strictness later as our confidence increases.An example error:
Impure values are allowed to flow into refs, meaning that we now allow
useRef(Date.now())oruseRef(localFunctionThatReturnsMathDotRandom())which would have errored previously. The next PR reuses this improved impurity tracking to validate ref access in render as well.Refs Now Treated As Impure Values in Render
Reading a ref now produces an
Impureeffect, and reading refs in render is validated using the above validation against impure values in render. The error category and message is customized for refs, we're just reusing the validation implementation. This means you get consistent results forperformance.now()as forref.current. A nice consistency win.Simplified writing-ref validation
Now that reading a ref in render is validated using the impure values infra, I also dramatically simplified ValidateNoRefAccessInRender to focus solely on validation against writing refs during render. It was harder to use the new effects infra for this since we intentionally do not record ref mutations as a
Mutateeffect. So for now, the pass switches on InstructionValue variants. We continue to support theif (ref.current == null) { ref.current = <init> }pattern and reasonable variants of it. We're conservative about what we consider to be a write of a ref -foo(ref)now assumes you're not mutating rather than the inverse.Takeaways
x = foo(ref)optimistically assumes you don't read the ref. Concretely, this follows from not tracking impure values across MaybeCapture effects.ref.current = <value>orref.current.property = <value>, and allow potential writes through egwriteToRef(ref, value).Stack created with Sapling. Best reviewed with ReviewStack.