Allow global mutation within useEffect (#2646)
Summary: Currently Forget bails on mutations to globals within any callback function. However, callbacks passed to useEffect should not bail and are not subject to the rules of react in the same way. We allow this by instead of immediately raising errors when we see illegal writes, storing the error as part of the function. When the function is called, or passed to a position that could call it during rendering, we bail as before; but if it's passed to `useEffect`, we don't raise the errors.
Michael Vitousek committed
Mar 5, 2024 at 11:54 UTC
22ea72d4e909e607d79bca8b46f09ded00153e06
9 files changed
+349
-51
compiler/packages/babel-plugin-react-forget/src/CompilerError.ts
+6
@@ -165,6 +165,12 @@ export class CompilerError extends Error {
165
throw errors;
166
}
167
168
+ static throw(options: CompilerErrorDetailOptions): never {
169
+ const errors = new CompilerError();
170
+ errors.pushErrorDetail(new CompilerErrorDetail(options));
171
+ throw errors;
172
+ }
173
+
174
constructor(...args: any[]) {
175
super(...args);
176
this.name = "ReactForgetCompilerError";
compiler/packages/babel-plugin-react-forget/src/HIR/BuildHIR.ts
+1
@@ -216,6 +216,7 @@ export function lower(
216
async: func.node.async === true,
217
loc: func.node.loc ?? GeneratedSource,
218
env,
219
+ effects: null,
220
});
221
}
222
compiler/packages/babel-plugin-react-forget/src/HIR/HIR.ts
+7
-1
@@ -6,7 +6,7 @@
6
*/
7
8
import * as t from "@babel/types";
9
-import { CompilerError } from "../CompilerError";
9
+import { CompilerError, CompilerErrorDetailOptions } from "../CompilerError";
10
import { assertExhaustive } from "../Utils/utils";
11
import { Environment } from "./Environment";
12
import { HookKind } from "./ObjectShape";
@@ -244,11 +244,17 @@ export type HIRFunction = {
244
params: Array<Place | SpreadPattern>;
245
returnType: t.FlowType | t.TSType | null;
246
context: Array<Place>;
247
+ effects: Array<FunctionEffect> | null;
248
body: HIR;
249
generator: boolean;
250
async: boolean;
251
};
252
253
+export type FunctionEffect = {
254
+ kind: "GlobalMutation";
255
+ error: CompilerErrorDetailOptions;
256
+};
257
+
258
/*
259
* Each reactive scope may have its own control-flow, so the instructions form
260
* a control-flow graph. The graph comprises a set of basic blocks which reference
compiler/packages/babel-plugin-react-forget/src/Inference/InferReferenceEffects.ts
+232
-49
@@ -5,7 +5,7 @@
5
* LICENSE file in the root directory of this source tree.
6
*/
7
8
-import { CompilerError } from "../CompilerError";
8
+import { CompilerError, ErrorSeverity } from "../CompilerError";
9
import { Environment } from "../HIR";
10
import {
11
AbstractValue,
@@ -13,6 +13,7 @@ import {
13
BlockId,
14
CallExpression,
15
Effect,
16
+ FunctionEffect,
17
GeneratedSource,
18
HIRFunction,
19
IdentifierId,
@@ -43,6 +44,7 @@ import {
44
eachTerminalSuccessor,
45
} from "../HIR/visitors";
46
import { assertExhaustive } from "../Utils/utils";
47
+import { isEffectHook } from "../Validation/ValidateMemoizedEffectDependencies";
48
49
const UndefinedValue: InstructionValue = {
50
kind: "Primitive",
@@ -206,6 +208,8 @@ export default function inferReferenceEffects(
208
}
209
queue(fn.body.entry, initialState);
210
211
+ const functionEffects: Array<FunctionEffect> = fn.effects ?? [];
212
+
213
while (queuedStates.size !== 0) {
214
for (const [blockId, block] of fn.body.blocks) {
215
const incomingState = queuedStates.get(blockId);
@@ -216,13 +220,29 @@ export default function inferReferenceEffects(
220
221
statesByBlock.set(blockId, incomingState);
222
const state = incomingState.clone();
219
- inferBlock(fn.env, state, block);
223
+ inferBlock(fn.env, functionEffects, state, block);
224
225
for (const nextBlockId of eachTerminalSuccessor(block.terminal)) {
226
queue(nextBlockId, state);
227
}
228
}
229
}
230
+
231
+ if (!options.isFunctionExpression) {
232
+ functionEffects.forEach((eff) => {
233
+ switch (eff.kind) {
234
+ case "GlobalMutation":
235
+ CompilerError.throw(eff.error);
236
+ default:
237
+ assertExhaustive(
238
+ eff.kind,
239
+ `Unexpected function effect kind '${eff.kind}'`
240
+ );
241
+ }
242
+ });
243
+ } else {
244
+ fn.effects = functionEffects;
245
+ }
246
}
247
248
// Maintains a mapping of top-level variables to the kind of value they hold
@@ -340,7 +360,12 @@ class InferenceState {
360
* Similarly, a freeze reference is converted to readonly if the
361
* value is already frozen or is immutable.
362
*/
343
- reference(place: Place, effectKind: Effect, reason: ValueReason): void {
363
+ reference(
364
+ place: Place,
365
+ functionEffects: Array<FunctionEffect>,
366
+ effectKind: Effect,
367
+ reason: ValueReason
368
+ ): void {
369
const values = this.#variables.get(place.identifier.id);
370
if (values === undefined) {
371
CompilerError.invariant(effectKind !== Effect.Store, {
@@ -355,6 +380,16 @@ class InferenceState {
380
: Effect.Read;
381
return;
382
}
383
+
384
+ for (const value of values) {
385
+ if (
386
+ (value.kind === "FunctionExpression" ||
387
+ value.kind === "ObjectMethod") &&
388
+ value.loweredFunc.func.effects != null
389
+ ) {
390
+ functionEffects.push(...value.loweredFunc.func.effects);
391
+ }
392
+ }
393
let valueKind: AbstractValue | null = this.kind(place);
394
let effect: Effect | null = null;
395
switch (effectKind) {
@@ -382,7 +417,12 @@ class InferenceState {
417
) {
418
if (value.kind === "FunctionExpression") {
419
for (const operand of eachInstructionValueOperand(value)) {
385
- this.reference(operand, Effect.Freeze, ValueReason.Other);
420
+ this.reference(
421
+ operand,
422
+ functionEffects,
423
+ Effect.Freeze,
424
+ ValueReason.Other
425
+ );
426
}
427
}
428
}
@@ -405,22 +445,25 @@ class InferenceState {
445
}
446
case Effect.Mutate: {
447
if (
408
- valueKind.kind === ValueKind.Mutable ||
409
- valueKind.kind === ValueKind.Context
448
+ valueKind.kind !== ValueKind.Mutable &&
449
+ valueKind.kind !== ValueKind.Context
450
) {
411
- effect = Effect.Mutate;
412
- } else {
451
let reason = getWriteErrorReason(valueKind);
414
- CompilerError.throwInvalidReact({
415
- reason,
416
- description:
417
- place.identifier.name !== null
418
- ? `Found mutation of ${place.identifier.name}`
419
- : null,
420
- loc: place.loc,
421
- suggestions: null,
452
+ functionEffects.push({
453
+ kind: "GlobalMutation",
454
+ error: {
455
+ reason,
456
+ description:
457
+ place.identifier.name !== null
458
+ ? `Found mutation of ${place.identifier.name}`
459
+ : null,
460
+ loc: place.loc,
461
+ suggestions: null,
462
+ severity: ErrorSeverity.InvalidReact,
463
+ },
464
});
465
}
466
+ effect = Effect.Mutate;
467
break;
468
}
469
case Effect.Store: {
@@ -429,15 +472,18 @@ class InferenceState {
472
valueKind.kind !== ValueKind.Context
473
) {
474
let reason = getWriteErrorReason(valueKind);
432
-
433
- CompilerError.throwInvalidReact({
434
- reason,
435
- description:
436
- place.identifier.name !== null
437
- ? `Found mutation of ${place.identifier.name}`
438
- : null,
439
- loc: place.loc,
440
- suggestions: null,
475
+ functionEffects.push({
476
+ kind: "GlobalMutation",
477
+ error: {
478
+ reason,
479
+ description:
480
+ place.identifier.name !== null
481
+ ? `Found mutation of ${place.identifier.name}`
482
+ : null,
483
+ loc: place.loc,
484
+ suggestions: null,
485
+ severity: ErrorSeverity.InvalidReact,
486
+ },
487
});
488
}
489
@@ -767,6 +813,7 @@ function mergeAbstractValues(
813
*/
814
function inferBlock(
815
env: Environment,
816
+ functionEffects: Array<FunctionEffect>,
817
state: InferenceState,
818
block: BasicBlock
819
): void {
@@ -828,6 +875,7 @@ function inferBlock(
875
// Object keys must be primitives, so we know they're frozen at this point
876
state.reference(
877
property.key.name,
878
+ functionEffects,
879
Effect.Freeze,
880
ValueReason.Other
881
);
@@ -835,6 +883,7 @@ function inferBlock(
883
// Object construction captures but does not modify the key/property values
884
state.reference(
885
property.place,
886
+ functionEffects,
887
Effect.Capture,
888
ValueReason.Other
889
);
@@ -844,6 +893,7 @@ function inferBlock(
893
// Object construction captures but does not modify the key/property values
894
state.reference(
895
property.place,
896
+ functionEffects,
897
Effect.Capture,
898
ValueReason.Other
899
);
@@ -954,6 +1004,7 @@ function inferBlock(
1004
for (const operand of eachInstructionOperand(instr)) {
1005
state.reference(
1006
operand,
1007
+ functionEffects,
1008
operand.effect === Effect.Unknown ? Effect.Read : operand.effect,
1009
ValueReason.Other
1010
);
@@ -990,29 +1041,49 @@ function inferBlock(
1041
}
1042
: { kind: ValueKind.Mutable, reason: new Set([ValueReason.Other]) };
1043
let hasCaptureArgument = false;
1044
+ let isUseEffect = isEffectHook(instrValue.callee.identifier);
1045
for (let i = 0; i < instrValue.args.length; i++) {
1046
+ const argumentEffects: Array<FunctionEffect> = [];
1047
const arg = instrValue.args[i];
1048
const place = arg.kind === "Identifier" ? arg : arg.place;
1049
if (effects !== null) {
997
- state.reference(place, effects[i], ValueReason.Other);
1050
+ state.reference(
1051
+ place,
1052
+ argumentEffects,
1053
+ effects[i],
1054
+ ValueReason.Other
1055
+ );
1056
} else {
1057
state.reference(
1058
place,
1059
+ argumentEffects,
1060
Effect.ConditionallyMutate,
1061
ValueReason.Other
1062
);
1063
}
1064
+ /*
1065
+ * Join the effects of the argument with the effects of the enclosing function,
1066
+ * unless the we're detecting a global mutation inside a useEffect hook
1067
+ */
1068
+ functionEffects.push(
1069
+ ...argumentEffects.filter(
1070
+ (argEffect) =>
1071
+ !isUseEffect || i !== 0 || argEffect.kind !== "GlobalMutation"
1072
+ )
1073
+ );
1074
hasCaptureArgument ||= place.effect === Effect.Capture;
1075
}
1076
if (signature !== null) {
1077
state.reference(
1078
instrValue.callee,
1079
+ functionEffects,
1080
signature.calleeEffect,
1081
ValueReason.Other
1082
);
1083
} else {
1084
state.reference(
1085
instrValue.callee,
1086
+ functionEffects,
1087
Effect.ConditionallyMutate,
1088
ValueReason.Other
1089
);
@@ -1034,7 +1105,12 @@ function inferBlock(
1105
loc: instrValue.loc,
1106
suggestions: null,
1107
});
1037
- state.reference(instrValue.property, Effect.Read, ValueReason.Other);
1108
+ state.reference(
1109
+ instrValue.property,
1110
+ functionEffects,
1111
+ Effect.Read,
1112
+ ValueReason.Other
1113
+ );
1114
1115
const signature = getFunctionCallSignature(
1116
env,
@@ -1060,10 +1136,16 @@ function inferBlock(
1136
*/
1137
for (const arg of instrValue.args) {
1138
const place = arg.kind === "Identifier" ? arg : arg.place;
1063
- state.reference(place, Effect.Read, ValueReason.Other);
1139
+ state.reference(
1140
+ place,
1141
+ functionEffects,
1142
+ Effect.Read,
1143
+ ValueReason.Other
1144
+ );
1145
}
1146
state.reference(
1147
instrValue.receiver,
1148
+ functionEffects,
1149
Effect.Capture,
1150
ValueReason.Other
1151
);
@@ -1087,10 +1169,16 @@ function inferBlock(
1169
* If effects are inferred for an argument, we should fail invalid
1170
* mutating effects
1171
*/
1090
- state.reference(place, effects[i], ValueReason.Other);
1172
+ state.reference(
1173
+ place,
1174
+ functionEffects,
1175
+ effects[i],
1176
+ ValueReason.Other
1177
+ );
1178
} else {
1179
state.reference(
1180
place,
1181
+ functionEffects,
1182
Effect.ConditionallyMutate,
1183
ValueReason.Other
1184
);
@@ -1100,12 +1188,14 @@ function inferBlock(
1188
if (signature !== null) {
1189
state.reference(
1190
instrValue.receiver,
1191
+ functionEffects,
1192
signature.calleeEffect,
1193
ValueReason.Other
1194
);
1195
} else {
1196
state.reference(
1197
instrValue.receiver,
1198
+ functionEffects,
1199
Effect.ConditionallyMutate,
1200
ValueReason.Other
1201
);
@@ -1124,8 +1214,18 @@ function inferBlock(
1214
state.kind(instrValue.object).kind === ValueKind.Context
1215
? Effect.ConditionallyMutate
1216
: Effect.Capture;
1127
- state.reference(instrValue.value, effect, ValueReason.Other);
1128
- state.reference(instrValue.object, Effect.Store, ValueReason.Other);
1217
+ state.reference(
1218
+ instrValue.value,
1219
+ functionEffects,
1220
+ effect,
1221
+ ValueReason.Other
1222
+ );
1223
+ state.reference(
1224
+ instrValue.object,
1225
+ functionEffects,
1226
+ Effect.Store,
1227
+ ValueReason.Other
1228
+ );
1229
1230
const lvalue = instr.lvalue;
1231
state.alias(lvalue, instrValue.value);
@@ -1142,7 +1242,12 @@ function inferBlock(
1242
break;
1243
}
1244
case "PropertyLoad": {
1145
- state.reference(instrValue.object, Effect.Read, ValueReason.Other);
1245
+ state.reference(
1246
+ instrValue.object,
1247
+ functionEffects,
1248
+ Effect.Read,
1249
+ ValueReason.Other
1250
+ );
1251
const lvalue = instr.lvalue;
1252
lvalue.effect = Effect.ConditionallyMutate;
1253
state.initialize(instrValue, state.kind(instrValue.object));
@@ -1154,9 +1259,24 @@ function inferBlock(
1259
state.kind(instrValue.object).kind === ValueKind.Context
1260
? Effect.ConditionallyMutate
1261
: Effect.Capture;
1157
- state.reference(instrValue.value, effect, ValueReason.Other);
1158
- state.reference(instrValue.property, Effect.Capture, ValueReason.Other);
1159
- state.reference(instrValue.object, Effect.Store, ValueReason.Other);
1262
+ state.reference(
1263
+ instrValue.value,
1264
+ functionEffects,
1265
+ effect,
1266
+ ValueReason.Other
1267
+ );
1268
+ state.reference(
1269
+ instrValue.property,
1270
+ functionEffects,
1271
+ Effect.Capture,
1272
+ ValueReason.Other
1273
+ );
1274
+ state.reference(
1275
+ instrValue.object,
1276
+ functionEffects,
1277
+ Effect.Store,
1278
+ ValueReason.Other
1279
+ );
1280
1281
const lvalue = instr.lvalue;
1282
state.alias(lvalue, instrValue.value);
@@ -1164,8 +1284,18 @@ function inferBlock(
1284
continue;
1285
}
1286
case "ComputedDelete": {
1167
- state.reference(instrValue.object, Effect.Mutate, ValueReason.Other);
1168
- state.reference(instrValue.property, Effect.Read, ValueReason.Other);
1287
+ state.reference(
1288
+ instrValue.object,
1289
+ functionEffects,
1290
+ Effect.Mutate,
1291
+ ValueReason.Other
1292
+ );
1293
+ state.reference(
1294
+ instrValue.property,
1295
+ functionEffects,
1296
+ Effect.Read,
1297
+ ValueReason.Other
1298
+ );
1299
state.initialize(instrValue, {
1300
kind: ValueKind.Immutable,
1301
reason: new Set([ValueReason.Other]),
@@ -1175,8 +1305,18 @@ function inferBlock(
1305
continue;
1306
}
1307
case "ComputedLoad": {
1178
- state.reference(instrValue.object, Effect.Read, ValueReason.Other);
1179
- state.reference(instrValue.property, Effect.Read, ValueReason.Other);
1308
+ state.reference(
1309
+ instrValue.object,
1310
+ functionEffects,
1311
+ Effect.Read,
1312
+ ValueReason.Other
1313
+ );
1314
+ state.reference(
1315
+ instrValue.property,
1316
+ functionEffects,
1317
+ Effect.Read,
1318
+ ValueReason.Other
1319
+ );
1320
const lvalue = instr.lvalue;
1321
lvalue.effect = Effect.ConditionallyMutate;
1322
state.initialize(instrValue, state.kind(instrValue.object));
@@ -1192,6 +1332,7 @@ function inferBlock(
1332
*/
1333
state.reference(
1334
instrValue.value,
1335
+ functionEffects,
1336
Effect.ConditionallyMutate,
1337
ValueReason.Other
1338
);
@@ -1210,7 +1351,12 @@ function inferBlock(
1351
* ```
1352
*/
1353
state.initialize(instrValue, state.kind(instrValue.value));
1213
- state.reference(instrValue.value, Effect.Read, ValueReason.Other);
1354
+ state.reference(
1355
+ instrValue.value,
1356
+ functionEffects,
1357
+ Effect.Read,
1358
+ ValueReason.Other
1359
+ );
1360
const lvalue = instr.lvalue;
1361
lvalue.effect = Effect.ConditionallyMutate;
1362
state.alias(lvalue, instrValue.value);
@@ -1218,9 +1364,19 @@ function inferBlock(
1364
}
1365
case "Memoize": {
1366
if (env.config.enablePreserveExistingMemoizationGuarantees) {
1221
- state.reference(instrValue.value, Effect.Freeze, ValueReason.Other);
1367
+ state.reference(
1368
+ instrValue.value,
1369
+ functionEffects,
1370
+ Effect.Freeze,
1371
+ ValueReason.Other
1372
+ );
1373
} else {
1223
- state.reference(instrValue.value, Effect.Read, ValueReason.Other);
1374
+ state.reference(
1375
+ instrValue.value,
1376
+ functionEffects,
1377
+ Effect.Read,
1378
+ ValueReason.Other
1379
+ );
1380
}
1381
const lvalue = instr.lvalue;
1382
lvalue.effect = Effect.ConditionallyMutate;
@@ -1238,14 +1394,24 @@ function inferBlock(
1394
state.kind(lvalue).kind === ValueKind.Context
1395
? Effect.ConditionallyMutate
1396
: Effect.Capture;
1241
- state.reference(instrValue.place, effect, ValueReason.Other);
1397
+ state.reference(
1398
+ instrValue.place,
1399
+ functionEffects,
1400
+ effect,
1401
+ ValueReason.Other
1402
+ );
1403
lvalue.effect = Effect.ConditionallyMutate;
1404
// direct aliasing: `a = b`;
1405
state.alias(lvalue, instrValue.place);
1406
continue;
1407
}
1408
case "LoadContext": {
1248
- state.reference(instrValue.place, Effect.Capture, ValueReason.Other);
1409
+ state.reference(
1410
+ instrValue.place,
1411
+ functionEffects,
1412
+ Effect.Capture,
1413
+ ValueReason.Other
1414
+ );
1415
const lvalue = instr.lvalue;
1416
lvalue.effect = Effect.ConditionallyMutate;
1417
const valueKind = state.kind(instrValue.place);
@@ -1297,7 +1463,12 @@ function inferBlock(
1463
state.kind(instrValue.lvalue).kind === ValueKind.Context
1464
? Effect.ConditionallyMutate
1465
: Effect.Capture;
1300
- state.reference(instrValue.value, effect, ValueReason.Other);
1466
+ state.reference(
1467
+ instrValue.value,
1468
+ functionEffects,
1469
+ effect,
1470
+ ValueReason.Other
1471
+ );
1472
1473
const lvalue = instr.lvalue;
1474
state.alias(lvalue, instrValue.value);
@@ -1318,7 +1489,12 @@ function inferBlock(
1489
state.kind(instrValue.lvalue.place).kind === ValueKind.Context
1490
? Effect.ConditionallyMutate
1491
: Effect.Capture;
1321
- state.reference(instrValue.value, effect, ValueReason.Other);
1492
+ state.reference(
1493
+ instrValue.value,
1494
+ functionEffects,
1495
+ effect,
1496
+ ValueReason.Other
1497
+ );
1498
1499
const lvalue = instr.lvalue;
1500
state.alias(lvalue, instrValue.value);
@@ -1336,11 +1512,13 @@ function inferBlock(
1512
case "StoreContext": {
1513
state.reference(
1514
instrValue.value,
1515
+ functionEffects,
1516
Effect.ConditionallyMutate,
1517
ValueReason.Other
1518
);
1519
state.reference(
1520
instrValue.lvalue.place,
1521
+ functionEffects,
1522
Effect.Mutate,
1523
ValueReason.Other
1524
);
@@ -1361,7 +1539,12 @@ function inferBlock(
1539
break;
1540
}
1541
}
1364
- state.reference(instrValue.value, effect, ValueReason.Other);
1542
+ state.reference(
1543
+ instrValue.value,
1544
+ functionEffects,
1545
+ effect,
1546
+ ValueReason.Other
1547
+ );
1548
1549
const lvalue = instr.lvalue;
1550
state.alias(lvalue, instrValue.value);
@@ -1408,7 +1591,7 @@ function inferBlock(
1591
loc: instrValue.loc,
1592
suggestions: null,
1593
});
1411
- state.reference(operand, effect.kind, effect.reason);
1594
+ state.reference(operand, functionEffects, effect.kind, effect.reason);
1595
}
1596
1597
state.initialize(instrValue, valueKind);
@@ -1430,7 +1613,7 @@ function inferBlock(
1613
} else {
1614
effect = Effect.Read;
1615
}
1433
- state.reference(operand, effect, ValueReason.Other);
1616
+ state.reference(operand, functionEffects, effect, ValueReason.Other);
1617
}
1618
}
1619
compiler/packages/babel-plugin-react-forget/src/Validation/ValidateMemoizedEffectDependencies.ts
+1
-1
@@ -119,7 +119,7 @@ function isUnmemoized(operand: Identifier, scopes: Set<ScopeId>): boolean {
119
return operand.scope != null && !scopes.has(operand.scope.id);
120
}
121
122
-function isEffectHook(identifier: Identifier): boolean {
122
+export function isEffectHook(identifier: Identifier): boolean {
123
return (
124
isUseEffectHookType(identifier) ||
125
isUseLayoutEffectHookType(identifier) ||
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.not-useEffect-external-mutate.expect.md
new
+29
@@ -0,0 +1,29 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+let x = { a: 42 };
6
+
7
+function Component(props) {
8
+ foo(() => {
9
+ x.a = 10;
10
+ x.a = 20;
11
+ });
12
+}
13
+
14
+```
15
+
16
+
17
+## Error
18
+
19
+```
20
+ 3 | function Component(props) {
21
+ 4 | foo(() => {
22
+> 5 | x.a = 10;
23
+ | ^ [ReactForget] InvalidReact: Writing to a variable defined outside a component or hook is not allowed. Consider using an effect. (5:5)
24
+ 6 | x.a = 20;
25
+ 7 | });
26
+ 8 | }
27
+```
28
+
29
+
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.not-useEffect-external-mutate.js
new
+8
@@ -0,0 +1,8 @@
1
+let x = { a: 42 };
2
+
3
+function Component(props) {
4
+ foo(() => {
5
+ x.a = 10;
6
+ x.a = 20;
7
+ });
8
+}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/useEffect-external-mutate.expect.md
new
+51
@@ -0,0 +1,51 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import { useEffect } from "react";
6
+
7
+let x = { a: 42 };
8
+
9
+function Component(props) {
10
+ useEffect(() => {
11
+ x.a = 10;
12
+ });
13
+}
14
+
15
+export const FIXTURE_ENTRYPOINT = {
16
+ fn: Component,
17
+ params: [],
18
+};
19
+
20
+```
21
+
22
+## Code
23
+
24
+```javascript
25
+import { useEffect, unstable_useMemoCache as useMemoCache } from "react";
26
+
27
+let x = { a: 42 };
28
+
29
+function Component(props) {
30
+ const $ = useMemoCache(1);
31
+ let t0;
32
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
33
+ t0 = () => {
34
+ x.a = 10;
35
+ };
36
+ $[0] = t0;
37
+ } else {
38
+ t0 = $[0];
39
+ }
40
+ useEffect(t0);
41
+}
42
+
43
+export const FIXTURE_ENTRYPOINT = {
44
+ fn: Component,
45
+ params: [],
46
+};
47
+
48
+```
49
+
50
+### Eval output
51
+(kind: ok)
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/useEffect-external-mutate.js
new
+14
@@ -0,0 +1,14 @@
1
+import { useEffect } from "react";
2
+
3
+let x = { a: 42 };
4
+
5
+function Component(props) {
6
+ useEffect(() => {
7
+ x.a = 10;
8
+ });
9
+}
10
+
11
+export const FIXTURE_ENTRYPOINT = {
12
+ fn: Component,
13
+ params: [],
14
+};