Skip to content

Commit 4bb18bd

Browse files
committed
[compiler] Treat ref-like named objects as refs
If a component uses the `useRef` hook directly then we type it's return value as a ref. But if it's wrapped in a custom hook then we lose out on this type information as the compiler doesn't look at the hook definition. This has resulted in some false positives in our analysis like the ones reported in #29160 and #29196. This PR will treat objects named as `ref` or if their names end with the substring `Ref`, and contain a property named `current`, as React refs. ``` const ref = useMyRef(); const myRef = useMyRef2(); useEffect(() => { ref.current = ...; myRef.current = ...; }) ``` In the above example, `ref` and `myRef` will be treated as React refs.
1 parent 92219ff commit 4bb18bd

13 files changed

+525
-10
lines changed

compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts

+17
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,23 @@ const EnvironmentConfigSchema = z.object({
407407
* and identifiers have been changed.
408408
*/
409409
hookPattern: z.string().nullable().default(null),
410+
411+
/**
412+
* If enabled, this will treat objects named as `ref` or if their names end with the substring `Ref`,
413+
* and contain a property named `current`, as React refs.
414+
*
415+
* ```
416+
* const ref = useMyRef();
417+
* const myRef = useMyRef2();
418+
* useEffect(() => {
419+
* ref.current = ...;
420+
* myRef.current = ...;
421+
* })
422+
* ```
423+
*
424+
* Here the variables `ref` and `myRef` will be typed as Refs.
425+
*/
426+
enableTreatRefLikeIdentifiersAsRefs: z.boolean().nullable().default(false),
410427
});
411428

412429
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;

compiler/packages/babel-plugin-react-compiler/src/HIR/Types.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ export type PhiType = {
5757
};
5858
export type PropType = {
5959
kind: "Property";
60-
object: Type;
60+
objectType: Type;
61+
objectName: string;
6162
propertyName: string;
6263
};
6364

@@ -124,7 +125,8 @@ export function duplicateType(type: Type): Type {
124125
case "Property": {
125126
return {
126127
kind: "Property",
127-
object: duplicateType(type.object),
128+
objectType: duplicateType(type.objectType),
129+
objectName: type.objectName,
128130
propertyName: type.propertyName,
129131
};
130132
}
@@ -165,11 +167,13 @@ function objectMethodTypeEquals(tA: Type, tB: Type): boolean {
165167

166168
function propTypeEquals(tA: Type, tB: Type): boolean {
167169
if (tA.kind === "Property" && tB.kind === "Property") {
168-
if (!typeEquals(tA.object, tB.object)) {
170+
if (!typeEquals(tA.objectType, tB.objectType)) {
169171
return false;
170172
}
171173

172-
return tA.propertyName === tB.propertyName;
174+
return (
175+
tA.propertyName === tB.propertyName && tA.objectName === tB.objectName
176+
);
173177
}
174178

175179
return false;

compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts

+50-6
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ import { Environment } from "../HIR";
1111
import { lowerType } from "../HIR/BuildHIR";
1212
import {
1313
HIRFunction,
14+
Identifier,
15+
IdentifierId,
1416
Instruction,
1517
makeType,
18+
PropType,
1619
Type,
1720
typeEquals,
1821
TypeId,
@@ -24,6 +27,7 @@ import {
2427
BuiltInJsxId,
2528
BuiltInObjectId,
2629
BuiltInPropsId,
30+
BuiltInRefValueId,
2731
BuiltInUseRefId,
2832
} from "../HIR/ObjectShape";
2933
import { eachInstructionLValue, eachInstructionOperand } from "../HIR/visitors";
@@ -117,6 +121,7 @@ function* generate(
117121
}
118122
}
119123

124+
const names = new Map();
120125
for (const [_, block] of func.body.blocks) {
121126
for (const phi of block.phis) {
122127
yield equation(phi.type, {
@@ -126,13 +131,28 @@ function* generate(
126131
}
127132

128133
for (const instr of block.instructions) {
129-
yield* generateInstructionTypes(func.env, instr);
134+
yield* generateInstructionTypes(func.env, names, instr);
130135
}
131136
}
132137
}
133138

139+
function setName(
140+
names: Map<IdentifierId, string>,
141+
id: IdentifierId,
142+
name: Identifier
143+
): void {
144+
if (name.name?.kind === "named") {
145+
names.set(id, name.name.value);
146+
}
147+
}
148+
149+
function getName(names: Map<IdentifierId, string>, id: IdentifierId): string {
150+
return names.get(id) ?? "";
151+
}
152+
134153
function* generateInstructionTypes(
135154
env: Environment,
155+
names: Map<IdentifierId, string>,
136156
instr: Instruction
137157
): Generator<TypeEquation, void, undefined> {
138158
const { lvalue, value } = instr;
@@ -152,6 +172,7 @@ function* generateInstructionTypes(
152172
}
153173

154174
case "LoadLocal": {
175+
setName(names, lvalue.identifier.id, value.place.identifier);
155176
yield equation(left, value.place.identifier.type);
156177
break;
157178
}
@@ -250,7 +271,8 @@ function* generateInstructionTypes(
250271
case "PropertyLoad": {
251272
yield equation(left, {
252273
kind: "Property",
253-
object: value.object.identifier.type,
274+
objectType: value.object.identifier.type,
275+
objectName: getName(names, value.object.identifier.id),
254276
propertyName: value.property,
255277
});
256278
break;
@@ -278,7 +300,8 @@ function* generateInstructionTypes(
278300
const propertyName = String(i);
279301
yield equation(item.identifier.type, {
280302
kind: "Property",
281-
object: value.value.identifier.type,
303+
objectType: value.value.identifier.type,
304+
objectName: getName(names, value.value.identifier.id),
282305
propertyName,
283306
});
284307
} else {
@@ -294,7 +317,8 @@ function* generateInstructionTypes(
294317
) {
295318
yield equation(property.place.identifier.type, {
296319
kind: "Property",
297-
object: value.value.identifier.type,
320+
objectType: value.value.identifier.type,
321+
objectName: getName(names, value.value.identifier.id),
298322
propertyName: property.key.name,
299323
});
300324
}
@@ -342,11 +366,11 @@ function* generateInstructionTypes(
342366
yield equation(left, { kind: "Object", shapeId: BuiltInJsxId });
343367
break;
344368
}
369+
case "PropertyStore":
345370
case "DeclareLocal":
346371
case "NewExpression":
347372
case "RegExpLiteral":
348373
case "MetaProperty":
349-
case "PropertyStore":
350374
case "ComputedStore":
351375
case "ComputedLoad":
352376
case "TaggedTemplateExpression":
@@ -375,7 +399,21 @@ class Unifier {
375399

376400
unify(tA: Type, tB: Type): void {
377401
if (tB.kind === "Property") {
378-
const objectType = this.get(tB.object);
402+
if (
403+
this.env.config.enableTreatRefLikeIdentifiersAsRefs &&
404+
isRefLikeName(tB)
405+
) {
406+
this.unify(tB.objectType, {
407+
kind: "Object",
408+
shapeId: BuiltInUseRefId,
409+
});
410+
this.unify(tA, {
411+
kind: "Object",
412+
shapeId: BuiltInRefValueId,
413+
});
414+
return;
415+
}
416+
const objectType = this.get(tB.objectType);
379417
const propertyType = this.env.getPropertyType(
380418
objectType,
381419
tB.propertyName
@@ -483,3 +521,9 @@ class Unifier {
483521
return type;
484522
}
485523
}
524+
525+
const RefLikeNameRE = /^(?:[a-zA-Z$_][a-zA-Z$_0-9]*)Ref$|^ref$/;
526+
527+
function isRefLikeName(t: PropType): boolean {
528+
return RefLikeNameRE.test(t.objectName) && t.propertyName === "current";
529+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validatePreserveExistingMemoizationGuarantees
6+
import { useCallback, useRef } from "react";
7+
8+
function useCustomRef() {
9+
return useRef({ click: () => {} });
10+
}
11+
12+
function Foo() {
13+
const Ref = useCustomRef();
14+
15+
const onClick = useCallback(() => {
16+
Ref.current?.click();
17+
}, []);
18+
19+
return <button onClick={onClick} />;
20+
}
21+
22+
export const FIXTURE_ENTRYPOINT = {
23+
fn: Foo,
24+
params: [],
25+
isComponent: true,
26+
};
27+
28+
```
29+
30+
31+
## Error
32+
33+
```
34+
9 | const Ref = useCustomRef();
35+
10 |
36+
> 11 | const onClick = useCallback(() => {
37+
| ^^^^^^^
38+
> 12 | Ref.current?.click();
39+
| ^^^^^^^^^^^^^^^^^^^^^^^^^
40+
> 13 | }, []);
41+
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (11:13)
42+
14 |
43+
15 | return <button onClick={onClick} />;
44+
16 | }
45+
```
46+
47+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// @validatePreserveExistingMemoizationGuarantees
2+
import { useCallback, useRef } from "react";
3+
4+
function useCustomRef() {
5+
return useRef({ click: () => {} });
6+
}
7+
8+
function Foo() {
9+
const Ref = useCustomRef();
10+
11+
const onClick = useCallback(() => {
12+
Ref.current?.click();
13+
}, []);
14+
15+
return <button onClick={onClick} />;
16+
}
17+
18+
export const FIXTURE_ENTRYPOINT = {
19+
fn: Foo,
20+
params: [],
21+
isComponent: true,
22+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validatePreserveExistingMemoizationGuarantees
6+
import { useCallback, useRef } from "react";
7+
8+
function useCustomRef() {
9+
return useRef({ click: () => {} });
10+
}
11+
12+
function Foo() {
13+
const notaref = useCustomRef();
14+
15+
const onClick = useCallback(() => {
16+
notaref.current?.click();
17+
}, []);
18+
19+
return <button onClick={onClick} />;
20+
}
21+
22+
export const FIXTURE_ENTRYPOINT = {
23+
fn: Foo,
24+
params: [],
25+
isComponent: true,
26+
};
27+
28+
```
29+
30+
31+
## Error
32+
33+
```
34+
9 | const notaref = useCustomRef();
35+
10 |
36+
> 11 | const onClick = useCallback(() => {
37+
| ^^^^^^^
38+
> 12 | notaref.current?.click();
39+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
40+
> 13 | }, []);
41+
| ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (11:13)
42+
14 |
43+
15 | return <button onClick={onClick} />;
44+
16 | }
45+
```
46+
47+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// @validatePreserveExistingMemoizationGuarantees
2+
import { useCallback, useRef } from "react";
3+
4+
function useCustomRef() {
5+
return useRef({ click: () => {} });
6+
}
7+
8+
function Foo() {
9+
const notaref = useCustomRef();
10+
11+
const onClick = useCallback(() => {
12+
notaref.current?.click();
13+
}, []);
14+
15+
return <button onClick={onClick} />;
16+
}
17+
18+
export const FIXTURE_ENTRYPOINT = {
19+
fn: Foo,
20+
params: [],
21+
isComponent: true,
22+
};

0 commit comments

Comments
 (0)