Skip to content

Conversation

josephsavona
Copy link
Member

@josephsavona josephsavona commented Aug 22, 2025

Partial fix for #34262. Consider this example:

function useInputValue(input) {
  const object = React.useMemo(() => {
    const {value} = transform(input);
    return {value};
  }, [input]);
  return object;
}

React Compiler breaks this code into two reactive scopes:

  • One for transform(input)
  • One for {value}

When we run ValidatePreserveExistingMemo, we see that the scope for {value} has the dependency value, whereas the original memoization had the dependency input, and throw an error that the dependencies didn't match.

In other words, we're flagging the fact that memoized better than the user as a problem. The more complete solution would be to validate that there is a subgraph of reactive scopes with a single input and output node, where the input node has the same dependencies as the original useMemo, and the output has the same outputs. That is true in this case, with the subgraph being the two consecutive scopes mentioned above.

But that's complicated. As a shortcut, this PR checks for any dependencies that are defined after the start of the original useMemo. If we find one, we know that it's a case where we were able to memoize more precisely than the original, and we don't report an error on the dependency. We still check that the original output value is able to be memoized, though. So if the scope of object were extended, eg with a call to mutate(object), then we'd still correctly report an error that we couldn't preserve memoization.


Stack created with Sapling. Best reviewed with ReviewStack.

@meta-cla meta-cla bot added the CLA Signed label Aug 22, 2025
@github-actions github-actions bot added the React Core Team Opened by a member of the React Core Team label Aug 22, 2025
@josephsavona josephsavona force-pushed the pr34276 branch 2 times, most recently from 052f2c9 to 0f632e5 Compare August 22, 2025 21:24
@josephsavona josephsavona requested a review from mofeiZ August 22, 2025 21:27
@@ -271,6 +275,13 @@ function validateInferredDep(
return;
}
}
if (
dep.identifier.mutableRange.start > memoStartInstruction &&
!isUnmemoized(dep.identifier, scopes)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isUnmemoized() check here is for sanity. I can't construct an example where it wouldn't be memoized, since the dep in this case would be constructed within the useMemo block. The only way to force such a dep to not memoize is if it somehow mixes up with a scope from outside the memo block, in which it would take on a dep that is mutated later which fails a separate validation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a fixture demonstrating that case just to be safe

Copy link
Member

@poteto poteto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I defer to @mofeiZ to review the actual fix, but the fixtures make sense to me

Comment on lines 12 to 14
* When we validate preserving manual memoization we incorrectly reject this, because
* the original memoization had `object` depending on `input` but our scope depends on
* `value`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this comment be updated, since it's now fixed?

@mofeiZ
Copy link
Contributor

mofeiZ commented Aug 27, 2025

Interesting, from a first read it feels like declsWithinMemoBlock is not tracking the correct set of declarations in this case. Will do a local checkout and try to understand the difference

@josephsavona
Copy link
Member Author

Interesting, from a first read it feels like declsWithinMemoBlock is not tracking the correct set of declarations in this case. Will do a local checkout and try to understand the difference

I don't think that's it. In the example from the description, the declarations set is correctly set to the {value} temporary.

@mofeiZ
Copy link
Contributor

mofeiZ commented Aug 27, 2025

#34319 here is an alternative implementation that uses declsWithinMemoBlock

josephsavona added a commit that referenced this pull request Aug 28, 2025
…ck (#34298)

In #34125 I added a hint where if you assign to the .current property of
a frozen object, we suggest naming the variable as `ref` or `-Ref`.
However, the tracking for mutations that assign to .current specifically
wasn't propagated past function expression boundaries, which meant that
the hint only showed up if you mutated the ref in the main body of the
component/hook. That's less likely to happen since most folks know not
to access refs in render. What's more likely is that you'll (correctly)
assign a ref in an effect or callback, but the compiler will throw an
error. By showing a hint in this case we can help people understand the
naming pattern.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34298).
* #34276
* __->__ #34298
github-actions bot pushed a commit that referenced this pull request Aug 28, 2025
…ck (#34298)

In #34125 I added a hint where if you assign to the .current property of
a frozen object, we suggest naming the variable as `ref` or `-Ref`.
However, the tracking for mutations that assign to .current specifically
wasn't propagated past function expression boundaries, which meant that
the hint only showed up if you mutated the ref in the main body of the
component/hook. That's less likely to happen since most folks know not
to access refs in render. What's more likely is that you'll (correctly)
assign a ref in an effect or callback, but the compiler will throw an
error. By showing a hint in this case we can help people understand the
naming pattern.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34298).
* #34276
* __->__ #34298

DiffTrain build for [8d7b5e4](8d7b5e4)
github-actions bot pushed a commit that referenced this pull request Aug 28, 2025
…ck (#34298)

In #34125 I added a hint where if you assign to the .current property of
a frozen object, we suggest naming the variable as `ref` or `-Ref`.
However, the tracking for mutations that assign to .current specifically
wasn't propagated past function expression boundaries, which meant that
the hint only showed up if you mutated the ref in the main body of the
component/hook. That's less likely to happen since most folks know not
to access refs in render. What's more likely is that you'll (correctly)
assign a ref in an effect or callback, but the compiler will throw an
error. By showing a hint in this case we can help people understand the
naming pattern.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34298).
* #34276
* __->__ #34298

DiffTrain build for [8d7b5e4](8d7b5e4)
Partial fix for #34262. Consider this example:

```js
function useInputValue(input) {
  const object = React.useMemo(() => {
    const {value} = transform(input);
    return {value};
  }, [input]);
  return object;
}
```

React Compiler breaks this code into two reactive scopes:
* One for `transform(input)`
* One for `{value}`

When we run ValidatePreserveExistingMemo, we see that the scope for `{value}` has the dependency `value`, whereas the original memoization had the dependency `input`, and throw an error that the dependencies didn't match.

In other words, we're flagging the fact that memoized _better than the user_ as a problem. The more complete solution would be to validate that there is a subgraph of reactive scopes with a single input and output node, where the input node has the same dependencies as the original useMemo, and the output has the same outputs. That is true in this case, with the subgraph being the two consecutive scopes mentioned above.

But that's complicated. As a shortcut, this PR checks for any dependencies that are defined after the start of the original useMemo. If we find one, we know that it's a case where we were able to memoize more precisely than the original, and we don't report an error on the dependency. We still check that the original _output_ value is able to be memoized, though. So if the scope of `object` were extended, eg with a call to `mutate(object)`, then we'd still correctly report an error that we couldn't preserve memoization.
github-actions bot pushed a commit to code/lib-react that referenced this pull request Aug 31, 2025
…ck (facebook#34298)

In facebook#34125 I added a hint where if you assign to the .current property of
a frozen object, we suggest naming the variable as `ref` or `-Ref`.
However, the tracking for mutations that assign to .current specifically
wasn't propagated past function expression boundaries, which meant that
the hint only showed up if you mutated the ref in the main body of the
component/hook. That's less likely to happen since most folks know not
to access refs in render. What's more likely is that you'll (correctly)
assign a ref in an effect or callback, but the compiler will throw an
error. By showing a hint in this case we can help people understand the
naming pattern.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34298).
* facebook#34276
* __->__ facebook#34298

DiffTrain build for [8d7b5e4](facebook@8d7b5e4)
github-actions bot pushed a commit to code/lib-react that referenced this pull request Aug 31, 2025
…ck (facebook#34298)

In facebook#34125 I added a hint where if you assign to the .current property of
a frozen object, we suggest naming the variable as `ref` or `-Ref`.
However, the tracking for mutations that assign to .current specifically
wasn't propagated past function expression boundaries, which meant that
the hint only showed up if you mutated the ref in the main body of the
component/hook. That's less likely to happen since most folks know not
to access refs in render. What's more likely is that you'll (correctly)
assign a ref in an effect or callback, but the compiler will throw an
error. By showing a hint in this case we can help people understand the
naming pattern.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34298).
* facebook#34276
* __->__ facebook#34298

DiffTrain build for [8d7b5e4](facebook@8d7b5e4)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants