Skip to content

[compiler] Lower ObjectMethod to FunctionExpression HIR for consistent per-function memoization#36225

Open
sleitor wants to merge 1 commit intofacebook:mainfrom
sleitor:fix-36151-v2
Open

[compiler] Lower ObjectMethod to FunctionExpression HIR for consistent per-function memoization#36225
sleitor wants to merge 1 commit intofacebook:mainfrom
sleitor:fix-36151-v2

Conversation

@sleitor
Copy link
Copy Markdown
Contributor

@sleitor sleitor commented Apr 6, 2026

Summary

Fixes #36151

React Compiler produces inconsistent memoization for objects depending on whether methods use shorthand syntax or function expression syntax:

  • Before: Method shorthand { foo() {} } → whole object memoized as one unit (less optimal)
  • After: Method shorthand { foo() {} } → each function memoized separately (same as { foo: () => {} })

The Fix

This PR takes the correct direction as suggested during review of #36224. Rather than merging FunctionExpression scopes into the ObjectExpression scope (which was the wrong approach in #36224), this PR lowers ObjectMethod HIR nodes to FunctionExpression HIR nodes during HIR construction in BuildHIR.ts.

Key changes

BuildHIR.ts:

  • lowerObjectMethod() now returns a FunctionExpression HIR node (with type: 'FunctionExpression') instead of an ObjectMethod HIR node
  • The ObjectProperty for the method now uses type: 'property' instead of type: 'method', so codegen emits it as a regular property with a function value ({ foo: function() {} })

AlignObjectMethodScopes.ts:

  • findScopesToMerge() is now a no-op since ObjectMethod nodes are no longer emitted. The scope merging was only needed because ObjectMethod nodes needed to be in the same reactive block as their enclosing ObjectExpression for correct codegen. Since lowering to FunctionExpression removes that constraint, no merging is needed.

Result

Method shorthands now flow through the same code path as arrow/function-expression properties, getting per-function reactive scopes automatically. This means:

// Before: whole object in one memo block
function useFoo() {
  const [state] = useState(false);
  return { func() { return state; } };
}

// After: func memoized separately based on its deps
function useFoo() {
  const [state] = useState(false);
  return {
    func: function() { return state; }  // memoized on [state]
  };
}

Tests

All existing tests pass (0 failures). 22 snapshot files were updated to reflect the improved memoization output.

Note: Supersedes the wrong-direction approach in #36224 (which is left open for reference).

Copy link
Copy Markdown
Contributor

@dimaMachina dimaMachina left a comment

Choose a reason for hiding this comment

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

nice clean fix, cc @josephsavona what do you think about this implementation?

method shorthand was introduced as syntactic sugar for function () {} so its safe treat them same in React compiler

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Compiler Bug]: Inconsistent memoization strategy for object with method shorthand vs function properties

2 participants