Files
react/compiler/packages/babel-plugin-react-compiler/docs/passes/49-validateNoRefAccessInRender.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

7.7 KiB

validateNoRefAccessInRender

File

src/Validation/ValidateNoRefAccessInRender.ts

Purpose

This validation pass ensures that React refs are not mutated during render. Refs are mutable containers for values that are not needed for rendering. Accessing or mutating ref.current during render can cause components to not update as expected because React does not track ref mutations.

The pass validates both direct ref mutations at the component level and ref mutations inside functions that are called during render.

Input Invariants

  • The function has been through type inference
  • Ref types are properly identified (useRef return values)
  • Function expressions have been lowered

Validation Rules

The pass produces errors for:

  1. Direct ref mutation in render: Assigning to ref.current at the top level of a component
  2. Ref mutation in render helper: Mutating a ref inside a function that is called during render
  3. Duplicate ref initialization: Initializing a ref more than once within null-guard blocks

Exception - Null-guard initialization pattern: The pass allows a single initialization of ref.current inside an if (ref.current == null) block. This is a common pattern for lazy initialization:

// ALLOWED - null-guard initialization
if (ref.current == null) {
  ref.current = expensiveComputation();
}

Error messages produced:

  • Category: Refs
  • Reason: "Cannot access refs during render"
  • Messages:
    • "Cannot update ref during render"
    • "Ref is initialized more than once during render"
    • "Ref was first initialized here" (for duplicate initialization)

Algorithm

Phase 1: Initialize Ref Tracking

Track refs from function parameters and context (captured variables):

for (const param of fn.params) {
  if (isUseRefType(place.identifier)) {
    refs.set(place.identifier.id, {kind: 'Ref', refId: makeRefId()});
  }
}

Phase 2: Single Forward Pass

Process all blocks in order, tracking:

  • refs: Map of identifier IDs to ref information
  • nullables: Set of identifiers known to be null/undefined
  • guards: Map of comparison results (e.g., ref.current == null)
  • safeBlocks: Map of blocks where null-guard allows initialization
  • refMutatingFunctions: Map of function identifiers that mutate refs

Phase 3: Process Instructions

For each instruction, handle:

switch (value.kind) {
  case 'PropertyLoad': {
    // Track ref.current access
    if (objRef?.kind === 'Ref' && value.property === 'current') {
      refs.set(lvalue.identifier.id, {kind: 'RefValue', refId: objRef.refId});
    }
    break;
  }
  case 'PropertyStore': {
    // Check for ref mutation
    if (isRef && isCurrentProperty && !isNullGuardInit) {
      if (isTopLevel) {
        errors.pushDiagnostic(makeRefMutationError(instr.loc));
      }
      return mutation;
    }
    break;
  }
  case 'FunctionExpression': {
    // Recursively validate with isTopLevel=false
    const mutation = validateFunction(..., false, errors);
    if (mutation != null) {
      refMutatingFunctions.set(lvalue.identifier.id, mutation);
    }
    break;
  }
  case 'CallExpression': {
    // Check if calling a ref-mutating function
    if (refMutatingFunctions.has(callee.identifier.id) && isTopLevel) {
      errors.pushDiagnostic(makeRefMutationError(mutationInfo.loc));
    }
    break;
  }
}

Phase 4: Guard Detection and Propagation

When encountering an if terminal with a null-guard condition:

if (block.terminal.kind === 'if') {
  const guard = guards.get(block.terminal.test.identifier.id);
  if (guard != null) {
    // For equality checks (==, ===), consequent is safe
    // For inequality checks (!=, !==), alternate is safe
    const safeBlock = guard.isEquality
      ? block.terminal.consequent
      : block.terminal.alternate;
    // Propagate safety through control flow
  }
}

Edge Cases

Null-Guard Initialization Pattern (Allowed)

function Component() {
  const ref = useRef(null);
  if (ref.current == null) {
    ref.current = computeValue();  // OK - first initialization
  }
  return <div />;
}

Duplicate Initialization (Error)

function Component() {
  const ref = useRef(null);
  if (ref.current == null) {
    ref.current = value1;  // First init - tracked
  }
  if (ref.current == null) {
    ref.current = value2;  // Error: duplicate initialization
  }
}

Negated Null Check

The pass correctly handles negated null checks:

if (ref.current !== null) {
  // NOT safe for initialization
} else {
  // Safe for initialization (ref.current is null here)
}

Ref Mutation in Called Function

function Component(props) {
  const ref = useRef(null);
  const renderItem = item => {
    ref.current = item;  // Mutation tracked in function
    return <Item item={item} />;
  };
  // Error: calling function that mutates ref during render
  return <List>{props.items.map(renderItem)}</List>;
}

Ref Mutation in Event Handler (Allowed)

function Component() {
  const ref = useRef(null);
  const onClick = () => {
    ref.current = value;  // OK - not called during render
  };
  return <button onClick={onClick} />;  // onClick is passed, not called
}

Arbitrary Comparison Values (Error)

Only null or undefined comparisons are recognized as null guards:

const DEFAULT_VALUE = 1;
if (ref.current == DEFAULT_VALUE) {
  ref.current = 1;  // Error: not a null guard
}

TODOs

None in the source file.

Example

Fixture: error.invalid-disallow-mutating-ref-in-render.js

Input:

// @validateRefAccessDuringRender
function Component() {
  const ref = useRef(null);
  ref.current = false;

  return <button ref={ref} />;
}

Error:

Found 1 error:

Error: Cannot access refs 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-disallow-mutating-ref-in-render.ts:4:2
  2 | function Component() {
  3 |   const ref = useRef(null);
> 4 |   ref.current = false;
    |   ^^^^^^^^^^^ Cannot update ref during render
  5 |
  6 |   return <button ref={ref} />;
  7 | }

Fixture: error.invalid-ref-in-callback-invoked-during-render.js

Input:

// @validateRefAccessDuringRender
function Component(props) {
  const ref = useRef(null);
  const renderItem = item => {
    const current = ref.current;
    return <Foo item={item} current={current} />;
  };
  return <Items>{props.items.map(item => renderItem(item))}</Items>;
}

Error:

Found 1 error:

Error: Cannot access ref value during render

React refs are values that are not needed for rendering...

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>;

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

Key observations:

  • Direct mutation at render level is an immediate error
  • Functions that mutate refs are tracked; errors occur when those functions are called at render level
  • The null-guard pattern allows a single initialization
  • The pass distinguishes between refs (useRef return type) and ref values (.current property)