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

9.9 KiB

validateHooksUsage

File

src/Validation/ValidateHooksUsage.ts

Purpose

This validation pass ensures that the function honors the Rules of Hooks. Specifically, it validates that:

  1. Hooks may only be called unconditionally (not in if statements, loops, etc.)
  2. Hooks cannot be used as first-class values (passed around, stored in variables, etc.)
  3. Hooks must be the same function on every render (no dynamic hooks)
  4. Hooks must be called at the top level, not within nested function expressions

Input Invariants

  • The function has been lowered to HIR
  • Global bindings have been resolved and typed
  • Nested function expressions have been lowered

Value Kinds Lattice

The pass uses abstract interpretation with a lattice of value kinds:

enum Kind {
  Error,        // Hook already used in an invalid way (stop reporting)
  KnownHook,    // Definitely a hook (from LoadGlobal with hook type)
  PotentialHook, // Might be a hook (hook-like name but not from global)
  Global,       // A global value that is not a hook
  Local,        // A local variable
}

The joinKinds function merges kinds, with earlier kinds taking precedence:

  • Error > KnownHook > PotentialHook > Global > Local

Validation Rules

Rule 1: No Conditional Hook Calls

Hooks must always be called in a consistent order.

Error:

Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)

Rule 2: No Hooks as First-Class Values

Known hooks may not be referenced as normal values (only called).

Error:

Error: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values

Rule 3: No Dynamic Hooks

Potential hooks (hook-like names from local scope) may change between renders.

Error:

Error: Hooks must be the same function on every render, but this value may change over time to a different function. See https://react.dev/reference/rules/react-calls-components-and-hooks#dont-dynamically-use-hooks

Rule 4: No Hooks in Nested Functions

Hooks must be called at the top level of a component or custom hook.

Error:

Error: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)

Cannot call [hookKind] within a function expression

Algorithm

Phase 1: Compute Unconditional Blocks

const unconditionalBlocks = computeUnconditionalBlocks(fn);

Determines which blocks are guaranteed to execute on every render (not inside conditionals).

Phase 2: Initialize Tracking

const valueKinds = new Map<IdentifierId, Kind>();

// Initialize parameters
for (const param of fn.params) {
  const place = param.kind === 'Identifier' ? param : param.place;
  const kind = getKindForPlace(place); // PotentialHook if hook-like name
  setKind(place, kind);
}

Phase 3: Track Value Kinds Through Instructions

For each instruction, the pass tracks how hook-ness flows through values:

case 'LoadGlobal':
  // Globals are the source of KnownHook
  if (getHookKind(fn.env, instr.lvalue.identifier) != null) {
    setKind(instr.lvalue, Kind.KnownHook);
  } else {
    setKind(instr.lvalue, Kind.Global);
  }
  break;

case 'PropertyLoad':
  // Hook-like property of Global -> KnownHook
  // Hook-like property of Local -> PotentialHook
  // Property of KnownHook -> KnownHook (if hook-like name)
  const objectKind = getKindForPlace(value.object);
  const isHookProperty = isHookName(value.property);
  // Determine kind based on object kind and property name
  break;

case 'CallExpression':
  const calleeKind = getKindForPlace(value.callee);
  const isHookCallee = calleeKind === Kind.KnownHook || calleeKind === Kind.PotentialHook;

  if (isHookCallee && !unconditionalBlocks.has(block.id)) {
    recordConditionalHookError(value.callee);
  } else if (calleeKind === Kind.PotentialHook) {
    recordDynamicHookUsageError(value.callee);
  }
  break;

Phase 4: Check for Invalid Hook References

When a KnownHook is used as an operand (not as a callee), it's an error:

function visitPlace(place: Place): void {
  const kind = valueKinds.get(place.identifier.id);
  if (kind === Kind.KnownHook) {
    recordInvalidHookUsageError(place);
  }
}

Phase 5: Validate Nested Function Expressions

Recursively check that nested functions don't call hooks:

function visitFunctionExpression(errors: CompilerError, fn: HIRFunction) {
  for (const instr of allInstructions(fn)) {
    if (isCall(instr)) {
      const callee = getCallee(instr);
      const hookKind = getHookKind(fn.env, callee.identifier);
      if (hookKind != null) {
        errors.push({
          reason: 'Hooks must be called at the top level...',
          description: `Cannot call ${hookKind} within a function expression`,
        });
      }
    }
    // Recursively check nested functions
    if (isFunctionExpression(instr)) {
      visitFunctionExpression(errors, instr.value.loweredFunc.func);
    }
  }
}

Phi Node Handling

For phi nodes (control flow join points), the pass joins the kinds of all operands:

for (const phi of block.phis) {
  let kind = isHookName(phi.place.identifier.name) ? Kind.PotentialHook : Kind.Local;
  for (const [, operand] of phi.operands) {
    const operandKind = valueKinds.get(operand.identifier.id);
    if (operandKind !== undefined) {
      kind = joinKinds(kind, operandKind);
    }
  }
  valueKinds.set(phi.place.identifier.id, kind);
}

Edge Cases

Optional Calls

Optional calls like useHook?.() are treated as conditional:

const result = useHook?.(); // Error: conditional hook call

Property Access on Hooks

Hook-like properties of known hooks are also known hooks:

const useFoo = useHook.useFoo; // useFoo is KnownHook
useFoo(); // Must be called unconditionally

Destructuring from Global

Destructuring hook-like names from a global creates known hooks:

const {useState} = React; // useState is KnownHook

Hook-Like Names from Local Variables

Hook-like names from local variables are potential hooks:

const obj = createObject();
const useFoo = obj.useFoo; // PotentialHook
useFoo(); // Error: dynamic hook

Error Deduplication

The pass deduplicates errors by source location, and once an error is recorded for a place, it's marked as Kind.Error to prevent further errors for the same place.

TODOs

  1. Fixpoint iteration for loops - The pass currently skips phi operands whose value is unknown (which can occur in loops). A follow-up could expand this to fixpoint iteration:
    // NOTE: we currently skip operands whose value is unknown
    // (which can only occur for functions with loops), we may
    // cause us to miss invalid code in some cases. We should
    // expand this to a fixpoint iteration in a follow-up.
    

Example

Fixture: rules-of-hooks/error.invalid-hook-if-consequent.js

Input:

function Component(props) {
  let x = null;
  if (props.cond) {
    x = useHook();
  }
  return x;
}

Error:

Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)

error.invalid-hook-if-consequent.ts:4:8
  2 |   let x = null;
  3 |   if (props.cond) {
> 4 |     x = useHook();
    |         ^^^^^^^ Hooks must always be called in a consistent order...
  5 |   }
  6 |   return x;

Fixture: rules-of-hooks/error.invalid-hook-as-prop.js

Input:

function Component({useFoo}) {
  useFoo();
}

Error:

Error: Hooks must be the same function on every render, but this value may change over time to a different function. See https://react.dev/reference/rules/react-calls-components-and-hooks#dont-dynamically-use-hooks

error.invalid-hook-as-prop.ts:2:2
  1 | function Component({useFoo}) {
> 2 |   useFoo();
    |   ^^^^^^ Hooks must be the same function on every render...
  3 | }

Fixture: rules-of-hooks/error.invalid-hook-in-nested-function-expression-object-expression.js

Input:

function Component() {
  'use memo';
  const f = () => {
    const x = {
      outer() {
        const g = () => {
          const y = {
            inner() {
              return useFoo();
            },
          };
          return y;
        };
      },
    };
    return x;
  };
}

Error:

Error: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)

Cannot call hook within a function expression.

error.invalid-hook-in-nested-function-expression-object-expression.ts:10:21
   8 |           const y = {
   9 |             inner() {
> 10 |               return useFoo();
     |                      ^^^^^^ Hooks must be called at the top level...
  11 |             },
  12 |           };

Fixture: rules-of-hooks/error.invalid-hook-optionalcall.js

Input:

function Component() {
  const {result} = useConditionalHook?.() ?? {};
  return result;
}

Error:

Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)

error.invalid-hook-optionalcall.ts:2:19
  1 | function Component() {
> 2 |   const {result} = useConditionalHook?.() ?? {};
    |                    ^^^^^^^^^^^^^^^^^^ Hooks must always be called in a consistent order...
  3 |   return result;