@samitouri / QOS-React-1 / commits / 22ea72d4e9

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 +};