Files
react/compiler/packages/babel-plugin-react-compiler/docs/passes/34-optimizePropsMethodCalls.md
Joseph Savona 870cccd656 [compiler] Summaries of the compiler passes to assist agents in development (#35595)
Autogenerated summaries of each of the compiler passes which allow
agents to get the key ideas of a compiler pass, including key
input/output invariants, without having to reprocess the file each time.
In the subsequent diff this seemed to help.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35595).
* #35607
* #35298
* #35596
* #35573
* __->__ #35595
* #35539
2026-01-23 11:26:47 -08:00

3.7 KiB

optimizePropsMethodCalls

File

src/Optimization/OptimizePropsMethodCalls.ts

Purpose

This pass converts method calls on the props object to regular function calls. Method calls like props.onClick() are transformed to const t0 = props.onClick; t0(). This normalization enables better analysis and optimization by the compiler.

The transformation is important because method calls have different semantics than regular calls - the receiver (props) would normally be passed as this to the method. For React props, methods are typically just callback functions where this binding doesn't matter, so converting them to regular calls is safe and enables better memoization.

Input Invariants

  • The function has been through type inference
  • Props parameters are typed as TObject<BuiltInProps>

Output Guarantees

  • All MethodCall instructions where the receiver has props type are converted to CallExpression
  • The method property becomes the callee of the call
  • Arguments are preserved exactly

Algorithm

export function optimizePropsMethodCalls(fn: HIRFunction): void {
  for (const [, block] of fn.body.blocks) {
    for (let i = 0; i < block.instructions.length; i++) {
      const instr = block.instructions[i]!;

      if (
        instr.value.kind === 'MethodCall' &&
        isPropsType(instr.value.receiver.identifier)
      ) {
        // Transform: props.onClick(arg)
        // To: const t0 = props.onClick; t0(arg)
        instr.value = {
          kind: 'CallExpression',
          callee: instr.value.property,  // The method becomes the callee
          args: instr.value.args,
          loc: instr.value.loc,
        };
      }
    }
  }
}

function isPropsType(identifier: Identifier): boolean {
  return (
    identifier.type.kind === 'Object' &&
    identifier.type.shapeId === BuiltInPropsId
  );
}

Edge Cases

Non-Props Method Calls

Method calls on non-props objects are left unchanged:

// Unchanged - array.map is not on props
array.map(x => x * 2)

// Unchanged - obj is not props
obj.method()

Props Type Detection

The pass uses type information to identify props:

function Component(props) {
  // props has type TObject<BuiltInProps>
  props.onClick();  // Transformed
}

function Regular(obj) {
  // obj has unknown type
  obj.onClick();  // Not transformed
}

Nested Props Access

Only direct method calls on props are transformed:

props.onClick();       // Transformed
props.nested.onClick(); // Not transformed (receiver is props.nested, not props)

Arrow Function Callbacks

Works with any method on props:

props.onChange(value);   // Transformed
props.onSubmit(data);    // Transformed
props.validate(input);   // Transformed

TODOs

None in the source file.

Example

Fixture: Using props method

Input:

function Component(props) {
  return <button onClick={() => props.onClick()} />;
}

Before OptimizePropsMethodCalls:

[1] $5 = Function @context[props$1] ...
    <<anonymous>>():
      [1] $2 = LoadLocal props$1
      [2] $3 = PropertyLoad $2.onClick
      [3] $4 = MethodCall $2.$3()  // Method call on props
      [4] Return Void

After OptimizePropsMethodCalls:

[1] $5 = Function @context[props$1] ...
    <<anonymous>>():
      [1] $2 = LoadLocal props$1
      [2] $3 = PropertyLoad $2.onClick
      [3] $4 = Call $3()  // Now a regular call
      [4] Return Void

Key observations:

  • MethodCall $2.$3() becomes Call $3()
  • The property load ($3 = PropertyLoad $2.onClick) is preserved
  • The receiver ($2) is no longer part of the call
  • This enables the compiler to analyze onClick as a regular function