Skip to content

Commit 8d7b5e4

Browse files
authored
[compiler] Show a ref name hint when assigning to non-ref in a callback (facebook#34298)
In facebook#34125 I added a hint where if you assign to the .current property of a frozen object, we suggest naming the variable as `ref` or `-Ref`. However, the tracking for mutations that assign to .current specifically wasn't propagated past function expression boundaries, which meant that the hint only showed up if you mutated the ref in the main body of the component/hook. That's less likely to happen since most folks know not to access refs in render. What's more likely is that you'll (correctly) assign a ref in an effect or callback, but the compiler will throw an error. By showing a hint in this case we can help people understand the naming pattern. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34298). * facebook#34276 * __->__ facebook#34298
1 parent 3434ff4 commit 8d7b5e4

File tree

5 files changed

+58
-2
lines changed

5 files changed

+58
-2
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -984,7 +984,7 @@ export function printAliasingEffect(effect: AliasingEffect): string {
984984
case 'MutateConditionally':
985985
case 'MutateTransitive':
986986
case 'MutateTransitiveConditionally': {
987-
return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}`;
987+
return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}${effect.kind === 'Mutate' && effect.reason?.kind === 'AssignCurrentProperty' ? ' (assign `.current`)' : ''}`;
988988
}
989989
case 'MutateFrozen': {
990990
return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;

compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
} from '../HIR/visitors';
2828
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
2929
import {Err, Ok, Result} from '../Utils/Result';
30-
import {AliasingEffect} from './AliasingEffects';
30+
import {AliasingEffect, MutationReason} from './AliasingEffects';
3131

3232
/**
3333
* This pass builds an abstract model of the heap and interprets the effects of the
@@ -101,6 +101,7 @@ export function inferMutationAliasingRanges(
101101
transitive: boolean;
102102
kind: MutationKind;
103103
place: Place;
104+
reason: MutationReason | null;
104105
}> = [];
105106
const renders: Array<{index: number; place: Place}> = [];
106107

@@ -176,6 +177,7 @@ export function inferMutationAliasingRanges(
176177
effect.kind === 'MutateTransitive'
177178
? MutationKind.Definite
178179
: MutationKind.Conditional,
180+
reason: null,
179181
place: effect.value,
180182
});
181183
} else if (
@@ -190,6 +192,7 @@ export function inferMutationAliasingRanges(
190192
effect.kind === 'Mutate'
191193
? MutationKind.Definite
192194
: MutationKind.Conditional,
195+
reason: effect.kind === 'Mutate' ? (effect.reason ?? null) : null,
193196
place: effect.value,
194197
});
195198
} else if (
@@ -241,6 +244,7 @@ export function inferMutationAliasingRanges(
241244
mutation.transitive,
242245
mutation.kind,
243246
mutation.place.loc,
247+
mutation.reason,
244248
errors,
245249
);
246250
}
@@ -267,6 +271,7 @@ export function inferMutationAliasingRanges(
267271
functionEffects.push({
268272
kind: 'Mutate',
269273
value: {...place, loc: node.local.loc},
274+
reason: node.mutationReason,
270275
});
271276
}
272277
}
@@ -507,6 +512,7 @@ export function inferMutationAliasingRanges(
507512
true,
508513
MutationKind.Conditional,
509514
into.loc,
515+
null,
510516
ignoredErrors,
511517
);
512518
for (const from of tracked) {
@@ -580,6 +586,7 @@ type Node = {
580586
transitive: {kind: MutationKind; loc: SourceLocation} | null;
581587
local: {kind: MutationKind; loc: SourceLocation} | null;
582588
lastMutated: number;
589+
mutationReason: MutationReason | null;
583590
value:
584591
| {kind: 'Object'}
585592
| {kind: 'Phi'}
@@ -599,6 +606,7 @@ class AliasingState {
599606
transitive: null,
600607
local: null,
601608
lastMutated: 0,
609+
mutationReason: null,
602610
value,
603611
});
604612
}
@@ -697,6 +705,7 @@ class AliasingState {
697705
transitive: boolean,
698706
startKind: MutationKind,
699707
loc: SourceLocation,
708+
reason: MutationReason | null,
700709
errors: CompilerError,
701710
): void {
702711
const seen = new Map<Identifier, MutationKind>();
@@ -717,6 +726,7 @@ class AliasingState {
717726
if (node == null) {
718727
continue;
719728
}
729+
node.mutationReason ??= reason;
720730
node.lastMutated = Math.max(node.lastMutated, index);
721731
if (end != null) {
722732
node.id.mutableRange.end = makeInstructionId(
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
2+
## Input
3+
4+
```javascript
5+
// Fixture to test that we show a hint to name as `ref` or `-Ref` when attempting
6+
// to assign .current inside an effect
7+
function Component({foo}) {
8+
useEffect(() => {
9+
foo.current = true;
10+
}, [foo]);
11+
}
12+
13+
```
14+
15+
16+
## Error
17+
18+
```
19+
Found 1 error:
20+
21+
Error: This value cannot be modified
22+
23+
Modifying component props or hook arguments is not allowed. Consider using a local variable instead.
24+
25+
error.assign-ref-in-effect-hint.ts:5:4
26+
3 | function Component({foo}) {
27+
4 | useEffect(() => {
28+
> 5 | foo.current = true;
29+
| ^^^ `foo` cannot be modified
30+
6 | }, [foo]);
31+
7 | }
32+
8 |
33+
34+
Hint: If this value is a Ref (value returned by `useRef()`), rename the variable to end in "Ref".
35+
```
36+
37+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Fixture to test that we show a hint to name as `ref` or `-Ref` when attempting
2+
// to assign .current inside an effect
3+
function Component({foo}) {
4+
useEffect(() => {
5+
foo.current = true;
6+
}, [foo]);
7+
}

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-mutate-context-in-callback.expect.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ error.invalid-mutate-context-in-callback.ts:12:4
3838
13 | };
3939
14 | return <div onClick={onClick} />;
4040
15 | }
41+
42+
Hint: If this value is a Ref (value returned by `useRef()`), rename the variable to end in "Ref".
4143
```
4244
4345

0 commit comments

Comments
 (0)