KEMBAR78
[compiler] Option to infer names for anonymous functions by josephsavona · Pull Request #34410 · facebook/react · GitHub
Skip to content

Conversation

josephsavona
Copy link
Member

@josephsavona josephsavona commented Sep 6, 2025

Adds a @enableNameAnonymousFunctions feature to infer helpful names for anonymous functions within components and hooks. The logic is inspired by a custom Next.js transform, flagged to us by @eps1lon, that does something similar. Implementing this transform within React Compiler means that all React (Compiler) users can benefit from more helpful names when debugging.

The idea builds on the fact that JS engines try to infer helpful names for anonymous functions (in stack traces) when those functions are accessed through an object property lookup:

({'a[xyz]': () => {
  throw new Error('hello!')
} }['a[xyz]'])()

// Stack trace:
Uncaught Error: hello!
    at a[xyz] (<anonymous>:1:26) // <-- note the name here
    at <anonymous>:1:60

The new NameAnonymousFunctions transform is gated by the above flag, which is off by default. It attemps to infer names for functions as follows:

First, determine a "local" name:

  • Assigning a function to a named variable uses the variable name. const f = () => {} gets the name "f".
  • Passing the function as an argument to a function gets the name of the function, ie foo(() => ...) get the name "foo()", foo.bar(() => ...) gets the name "foo.bar()". Note the parenthesis to help understand that it was part of a call.
  • Passing the function to a known hook uses the name of the hook, useEffect(() => ...) uses "useEffect()".
  • Passing the function as a JSX prop uses the element and attr name, eg <div onClick={() => ...} uses "
    .onClick".

Second, the local name is combined with the name of the outer component/hook, so the final names will be strings like Component[f] or useMyHook[useEffect()].


Stack created with Sapling. Best reviewed with ReviewStack.

@meta-cla meta-cla bot added the CLA Signed label Sep 6, 2025
@github-actions github-actions bot added the React Core Team Opened by a member of the React Core Team label Sep 6, 2025
@josephsavona
Copy link
Member Author

josephsavona commented Sep 7, 2025

Note that we prune useMemo/useCallback, which means if you have eg const onClick = useCallback(() => ...) the name will be Component[onClick], not Component[useCallback()] - similar for useMemo. The naming also only happens if the function gets compiled, though we could consider doing this as part of the retry pipeline built for fire.

@gaearon
Copy link
Collaborator

gaearon commented Sep 7, 2025

Passing the function to a known hook uses the name of the hook, useEffect(() => ...) uses "useEffect()".

I think we had a similar transform in WWW (maybe?) that is a bit more nuanced — it was more general (worked for any callbacks) and gave names like useEffect_arg0 or JSON_stringify_arg1. I liked that this convention lets you distinguish different arguments (when there's multiple function arguments). I also liked that it avoids giving the wrong idea that it's the useEffect hook itself that is on the stack. I know most React users might not think about this but IMO it's possible to get confused by it. Especially because an eager crash during a Hook call is also possible.

@gaearon
Copy link
Collaborator

gaearon commented Sep 7, 2025

You could also imagine this convention being nested, e.g. useEffect_arg0__JSON_stringify_arg1 for error in

useEffect(() => {
  const stuff = JSON.stringify(whatever, null, item => { /* crash here */ })
}, [])

@gaearon
Copy link
Collaborator

gaearon commented Sep 7, 2025

I see a point though that maybe parens are already enough to distinguish it, and otherwise arg may just be too noisy. I never complain about extra information in stack traces though.

@josephsavona
Copy link
Member Author

Interesting. For the nested case I could imagine something like Component[useEffect(arg0) > JSON.stringify(arg2)] which would be pretty helpful. I'll take a stab at it :-)

Comment on lines +75 to +77
const inner = { "Component[named > inner]": () => props.named }[
"Component[named > inner]"
];
Copy link
Member Author

Choose a reason for hiding this comment

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

nested anonymous functions assigned to variables

Copy link
Member Author

Choose a reason for hiding this comment

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

Note that if the inner function was originally named, then we wouldn't change that name. So if you have

useEffect(() => {
  function foo() {}
}, [...]);

We won't rename foo or wrap it in the object+property access structure. The case highlighted here for named > inner is happening because both of those are actually anonymous functions that are assigned to a variable.

const methodCall = SharedRuntime.identity(t2);
let t3;
if ($[6] !== props.call) {
t3 = { "Component[identity(arg0)]": () => props.call }[
Copy link
Member Author

Choose a reason for hiding this comment

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

argument indices for anonymous functions passed to calls

Comment on lines 176 to 180
"Component[useEffect(arg0) > JSON.stringify(arg2)]": () =>
props.useEffect,
}["Component[useEffect(arg0) > JSON.stringify(arg2)]"],
);
const g = { "Component[useEffect(arg0) > g]": () => props.useEffect }[
Copy link
Member Author

Choose a reason for hiding this comment

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

more cases of nested anonymous functions

@josephsavona
Copy link
Member Author

@gaearon done, what do you think?

@gaearon
Copy link
Collaborator

gaearon commented Sep 7, 2025

Ah sweet, I think this is nice! Hard to say how it'll feel without looking at some traces "from the real world" so I'd suggest giving it a try with some real code next to see if it's helpful.

@josephsavona
Copy link
Member Author

Yeah I think we need real-world feedback on this one. The feature is off by default so we can ship, collect feedback, and iterate.

@vcarl
Copy link
Contributor

vcarl commented Sep 7, 2025

Okay this is like my favorite thing I've seen in years, imagining the amount of time I would have saved by having anything to search for in situations like this and it's incredible to think about. To massively expand scope, could this plausibly evolve into a TC39 proposal?

@josephsavona
Copy link
Member Author

josephsavona commented Sep 7, 2025

To massively expand scope, could this plausibly evolve into a TC39 proposal?

I don’t think we’re likely to pursue a TC39 proposal. It would certainly be nice to not need an object + property lookup just to set a name hint, but we can do this in dev mode via bundler integrations. We’ll start there.

const namedElementAttr = t5;
let t6;
if ($[12] !== props.hookArgument) {
t6 = { "Component[useIdentity(arg0)]": () => props.hookArgument }[
Copy link
Collaborator

Choose a reason for hiding this comment

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

Looks like this is similar to the case for existing memoization. E.g. const onClick = useCallback(() => fn(), []). Any chance we can special case the React Hooks to get more accurate names? Next.js would've produced Component.useCallback[onClick] here instead of Component[useCallback(arg0)]

Copy link
Member Author

@josephsavona josephsavona Sep 8, 2025

Choose a reason for hiding this comment

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

Hmm, that doesn't really make sense for arbitrary hooks, useIdentity is a contrived function for testing. Note that the compiler strips out useCallback(), so your example would just become Component[onClick]

Copy link
Collaborator

Choose a reason for hiding this comment

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

I was under the impression that it only does that for equivalent memoization not always?

Copy link
Member Author

@josephsavona josephsavona Sep 8, 2025

Choose a reason for hiding this comment

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

One of two things will happen:

  • File is compiled. This only happens if the compiler can guarantee it preserved all the existing memoization. In this mode, any useMemo/useCallback are removed+rewritten, so the naming pass would see eg const onClick = () => {...} and name it Component[onClick].
  • File is not compiled, code is unmodified. We don't add any names.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah, that works perfectly then. Thanks for explaining.

Adds a `@enableNameAnonymousFunctions` feature to infer helpful names for anonymous functions within components and hooks. The logic is inspired by a custom Next.js transform, flagged to us by @eps1lon, that does something similar. Implementing this transform within React Compiler means that all React (Compiler) users can benefit from more helpful names when debugging.

The idea builds on the fact that JS engines try to infer helpful names for anonymous functions (in stack traces) when those functions are accessed through an object property lookup:

```js
({'a[xyz]': () => {
  throw new Error('hello!')
} }['a[xyz]'])()

// Stack trace:
Uncaught Error: hello!
    at a[xyz] (<anonymous>:1:26) // <-- note the name here
    at <anonymous>:1:60
```

The new NameAnonymousFunctions transform is gated by the above flag, which is off by default. It attemps to infer names for functions as follows:

First, determine a "local" name:
* Assigning a function to a named variable uses the variable name. `const f = () => {}` gets the name "f".
* Passing the function as an argument to a function gets the name of the function, ie `foo(() => ...)` get the name "foo()", `foo.bar(() => ...)` gets the name "foo.bar()". Note the parenthesis to help understand that it was part of a call.
* Passing the function to a known hook uses the name of the hook, `useEffect(() => ...)` uses "useEffect()".
* Passing the function as a JSX prop uses the element and attr name, eg `<div onClick={() => ...}` uses "<div>.onClick".

Second, the local name is combined with the name of the outer component/hook, so the final names will be strings like `Component[f]` or `useMyHook[useEffect()]`.
Copy link
Collaborator

@eps1lon eps1lon left a comment

Choose a reason for hiding this comment

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

🚀

@josephsavona josephsavona merged commit a9410fb into main Sep 9, 2025
21 checks passed
github-actions bot pushed a commit that referenced this pull request Sep 9, 2025
Adds a `@enableNameAnonymousFunctions` feature to infer helpful names
for anonymous functions within components and hooks. The logic is
inspired by a custom Next.js transform, flagged to us by @eps1lon, that
does something similar. Implementing this transform within React
Compiler means that all React (Compiler) users can benefit from more
helpful names when debugging.

The idea builds on the fact that JS engines try to infer helpful names
for anonymous functions (in stack traces) when those functions are
accessed through an object property lookup:

```js
({'a[xyz]': () => {
  throw new Error('hello!')
} }['a[xyz]'])()

// Stack trace:
Uncaught Error: hello!
    at a[xyz] (<anonymous>:1:26) // <-- note the name here
    at <anonymous>:1:60
```

The new NameAnonymousFunctions transform is gated by the above flag,
which is off by default. It attemps to infer names for functions as
follows:

First, determine a "local" name:
* Assigning a function to a named variable uses the variable name.
`const f = () => {}` gets the name "f".
* Passing the function as an argument to a function gets the name of the
function, ie `foo(() => ...)` get the name "foo()", `foo.bar(() => ...)`
gets the name "foo.bar()". Note the parenthesis to help understand that
it was part of a call.
* Passing the function to a known hook uses the name of the hook,
`useEffect(() => ...)` uses "useEffect()".
* Passing the function as a JSX prop uses the element and attr name, eg
`<div onClick={() => ...}` uses "<div>.onClick".

Second, the local name is combined with the name of the outer
component/hook, so the final names will be strings like `Component[f]`
or `useMyHook[useEffect()]`.

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

DiffTrain build for [a9410fb](a9410fb)
github-actions bot pushed a commit that referenced this pull request Sep 9, 2025
Adds a `@enableNameAnonymousFunctions` feature to infer helpful names
for anonymous functions within components and hooks. The logic is
inspired by a custom Next.js transform, flagged to us by @eps1lon, that
does something similar. Implementing this transform within React
Compiler means that all React (Compiler) users can benefit from more
helpful names when debugging.

The idea builds on the fact that JS engines try to infer helpful names
for anonymous functions (in stack traces) when those functions are
accessed through an object property lookup:

```js
({'a[xyz]': () => {
  throw new Error('hello!')
} }['a[xyz]'])()

// Stack trace:
Uncaught Error: hello!
    at a[xyz] (<anonymous>:1:26) // <-- note the name here
    at <anonymous>:1:60
```

The new NameAnonymousFunctions transform is gated by the above flag,
which is off by default. It attemps to infer names for functions as
follows:

First, determine a "local" name:
* Assigning a function to a named variable uses the variable name.
`const f = () => {}` gets the name "f".
* Passing the function as an argument to a function gets the name of the
function, ie `foo(() => ...)` get the name "foo()", `foo.bar(() => ...)`
gets the name "foo.bar()". Note the parenthesis to help understand that
it was part of a call.
* Passing the function to a known hook uses the name of the hook,
`useEffect(() => ...)` uses "useEffect()".
* Passing the function as a JSX prop uses the element and attr name, eg
`<div onClick={() => ...}` uses "<div>.onClick".

Second, the local name is combined with the name of the outer
component/hook, so the final names will be strings like `Component[f]`
or `useMyHook[useEffect()]`.

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

DiffTrain build for [a9410fb](a9410fb)
@eps1lon eps1lon deleted the pr34410 branch September 9, 2025 17:39
@eps1lon eps1lon restored the pr34410 branch September 9, 2025 17:39
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.

4 participants