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
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 (
useRefreturn values) - Function expressions have been lowered
Validation Rules
The pass produces errors for:
- Direct ref mutation in render: Assigning to
ref.currentat the top level of a component - Ref mutation in render helper: Mutating a ref inside a function that is called during render
- 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 informationnullables: Set of identifiers known to be null/undefinedguards: Map of comparison results (e.g.,ref.current == null)safeBlocks: Map of blocks where null-guard allows initializationrefMutatingFunctions: 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 (
useRefreturn type) and ref values (.currentproperty)