main
md 268 lines 7.71 KB
Rendered Raw
1 # validateNoRefAccessInRender
2
3 ## File
4 `src/Validation/ValidateNoRefAccessInRender.ts`
5
6 ## Purpose
7 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.
8
9 The pass validates both direct ref mutations at the component level and ref mutations inside functions that are called during render.
10
11 ## Input Invariants
12 - The function has been through type inference
13 - Ref types are properly identified (`useRef` return values)
14 - Function expressions have been lowered
15
16 ## Validation Rules
17 The pass produces errors for:
18
19 1. **Direct ref mutation in render**: Assigning to `ref.current` at the top level of a component
20 2. **Ref mutation in render helper**: Mutating a ref inside a function that is called during render
21 3. **Duplicate ref initialization**: Initializing a ref more than once within null-guard blocks
22
23 **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:
24
25 ```javascript
26 // ALLOWED - null-guard initialization
27 if (ref.current == null) {
28 ref.current = expensiveComputation();
29 }
30 ```
31
32 Error messages produced:
33 - Category: `Refs`
34 - Reason: "Cannot access refs during render"
35 - Messages:
36 - "Cannot update ref during render"
37 - "Ref is initialized more than once during render"
38 - "Ref was first initialized here" (for duplicate initialization)
39
40 ## Algorithm
41
42 ### Phase 1: Initialize Ref Tracking
43 Track refs from function parameters and context (captured variables):
44
45 ```typescript
46 for (const param of fn.params) {
47 if (isUseRefType(place.identifier)) {
48 refs.set(place.identifier.id, {kind: 'Ref', refId: makeRefId()});
49 }
50 }
51 ```
52
53 ### Phase 2: Single Forward Pass
54 Process all blocks in order, tracking:
55 - `refs`: Map of identifier IDs to ref information
56 - `nullables`: Set of identifiers known to be null/undefined
57 - `guards`: Map of comparison results (e.g., `ref.current == null`)
58 - `safeBlocks`: Map of blocks where null-guard allows initialization
59 - `refMutatingFunctions`: Map of function identifiers that mutate refs
60
61 ### Phase 3: Process Instructions
62 For each instruction, handle:
63
64 ```typescript
65 switch (value.kind) {
66 case 'PropertyLoad': {
67 // Track ref.current access
68 if (objRef?.kind === 'Ref' && value.property === 'current') {
69 refs.set(lvalue.identifier.id, {kind: 'RefValue', refId: objRef.refId});
70 }
71 break;
72 }
73 case 'PropertyStore': {
74 // Check for ref mutation
75 if (isRef && isCurrentProperty && !isNullGuardInit) {
76 if (isTopLevel) {
77 errors.pushDiagnostic(makeRefMutationError(instr.loc));
78 }
79 return mutation;
80 }
81 break;
82 }
83 case 'FunctionExpression': {
84 // Recursively validate with isTopLevel=false
85 const mutation = validateFunction(..., false, errors);
86 if (mutation != null) {
87 refMutatingFunctions.set(lvalue.identifier.id, mutation);
88 }
89 break;
90 }
91 case 'CallExpression': {
92 // Check if calling a ref-mutating function
93 if (refMutatingFunctions.has(callee.identifier.id) && isTopLevel) {
94 errors.pushDiagnostic(makeRefMutationError(mutationInfo.loc));
95 }
96 break;
97 }
98 }
99 ```
100
101 ### Phase 4: Guard Detection and Propagation
102 When encountering an `if` terminal with a null-guard condition:
103
104 ```typescript
105 if (block.terminal.kind === 'if') {
106 const guard = guards.get(block.terminal.test.identifier.id);
107 if (guard != null) {
108 // For equality checks (==, ===), consequent is safe
109 // For inequality checks (!=, !==), alternate is safe
110 const safeBlock = guard.isEquality
111 ? block.terminal.consequent
112 : block.terminal.alternate;
113 // Propagate safety through control flow
114 }
115 }
116 ```
117
118 ## Edge Cases
119
120 ### Null-Guard Initialization Pattern (Allowed)
121 ```javascript
122 function Component() {
123 const ref = useRef(null);
124 if (ref.current == null) {
125 ref.current = computeValue(); // OK - first initialization
126 }
127 return <div />;
128 }
129 ```
130
131 ### Duplicate Initialization (Error)
132 ```javascript
133 function Component() {
134 const ref = useRef(null);
135 if (ref.current == null) {
136 ref.current = value1; // First init - tracked
137 }
138 if (ref.current == null) {
139 ref.current = value2; // Error: duplicate initialization
140 }
141 }
142 ```
143
144 ### Negated Null Check
145 The pass correctly handles negated null checks:
146 ```javascript
147 if (ref.current !== null) {
148 // NOT safe for initialization
149 } else {
150 // Safe for initialization (ref.current is null here)
151 }
152 ```
153
154 ### Ref Mutation in Called Function
155 ```javascript
156 function Component(props) {
157 const ref = useRef(null);
158 const renderItem = item => {
159 ref.current = item; // Mutation tracked in function
160 return <Item item={item} />;
161 };
162 // Error: calling function that mutates ref during render
163 return <List>{props.items.map(renderItem)}</List>;
164 }
165 ```
166
167 ### Ref Mutation in Event Handler (Allowed)
168 ```javascript
169 function Component() {
170 const ref = useRef(null);
171 const onClick = () => {
172 ref.current = value; // OK - not called during render
173 };
174 return <button onClick={onClick} />; // onClick is passed, not called
175 }
176 ```
177
178 ### Arbitrary Comparison Values (Error)
179 Only `null` or `undefined` comparisons are recognized as null guards:
180 ```javascript
181 const DEFAULT_VALUE = 1;
182 if (ref.current == DEFAULT_VALUE) {
183 ref.current = 1; // Error: not a null guard
184 }
185 ```
186
187 ## TODOs
188 None in the source file.
189
190 ## Example
191
192 ### Fixture: `error.invalid-disallow-mutating-ref-in-render.js`
193
194 **Input:**
195 ```javascript
196 // @validateRefAccessDuringRender
197 function Component() {
198 const ref = useRef(null);
199 ref.current = false;
200
201 return <button ref={ref} />;
202 }
203 ```
204
205 **Error:**
206 ```
207 Found 1 error:
208
209 Error: Cannot access refs during render
210
211 React refs are values that are not needed for rendering. Refs should only be
212 accessed outside of render, such as in event handlers or effects. Accessing a
213 ref value (the `current` property) during render can cause your component not
214 to update as expected (https://react.dev/reference/react/useRef).
215
216 error.invalid-disallow-mutating-ref-in-render.ts:4:2
217 2 | function Component() {
218 3 | const ref = useRef(null);
219 > 4 | ref.current = false;
220 | ^^^^^^^^^^^ Cannot update ref during render
221 5 |
222 6 | return <button ref={ref} />;
223 7 | }
224 ```
225
226 ### Fixture: `error.invalid-ref-in-callback-invoked-during-render.js`
227
228 **Input:**
229 ```javascript
230 // @validateRefAccessDuringRender
231 function Component(props) {
232 const ref = useRef(null);
233 const renderItem = item => {
234 const current = ref.current;
235 return <Foo item={item} current={current} />;
236 };
237 return <Items>{props.items.map(item => renderItem(item))}</Items>;
238 }
239 ```
240
241 **Error:**
242 ```
243 Found 1 error:
244
245 Error: Cannot access ref value during render
246
247 React refs are values that are not needed for rendering...
248
249 error.invalid-ref-in-callback-invoked-during-render.ts:6:37
250 4 | const renderItem = item => {
251 5 | const current = ref.current;
252 > 6 | return <Foo item={item} current={current} />;
253 | ^^^^^^^ Ref value is used during render
254 7 | };
255 8 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
256
257 error.invalid-ref-in-callback-invoked-during-render.ts:5:20
258 3 | const ref = useRef(null);
259 4 | const renderItem = item => {
260 > 5 | const current = ref.current;
261 | ^^^^^^^^^^^ Ref is initially accessed
262 ```
263
264 Key observations:
265 - Direct mutation at render level is an immediate error
266 - Functions that mutate refs are tracked; errors occur when those functions are called at render level
267 - The null-guard pattern allows a single initialization
268 - The pass distinguishes between refs (`useRef` return type) and ref values (`.current` property)