Skip to content

Commit 41c1e1c

Browse files
committed
[compiler] Added check for if the same invalid setSate within an effect is used elsewhere
1 parent 89bec62 commit 41c1e1c

File tree

1 file changed

+61
-31
lines changed

1 file changed

+61
-31
lines changed

compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoDerivedComputationsInEffects.ts

Lines changed: 61 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ type SetStateCall = {
4040
};
4141
type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsOrState';
4242

43+
type SetStateName = string | undefined | null;
44+
4345
type DerivationMetadata = {
4446
typeOfValue: TypeOfValue;
4547
// TODO: Rename to place
@@ -51,8 +53,9 @@ type DerivationMetadata = {
5153
type ErrorMetadata = {
5254
errorType: 'HoistState' | 'CalculateInRender';
5355
propInfo: string | undefined;
56+
localStateInfo: string | undefined;
5457
loc: SourceLocation;
55-
setStateId: IdentifierId;
58+
setStateName: SetStateName;
5659
};
5760

5861
function joinValue(
@@ -92,7 +95,7 @@ function updateDerivationMetadata(
9295
function parseInstr(
9396
instr: Instruction,
9497
derivedTuple: Map<IdentifierId, DerivationMetadata>,
95-
setStateCalls: Map<string, Place>,
98+
setStateCalls: Map<SetStateName, Place[]>,
9699
) {
97100
// console.log(printInstruction(instr));
98101
// console.log(instr);
@@ -120,14 +123,17 @@ function parseInstr(
120123
isSetStateType(instr.value.callee.identifier) &&
121124
instr.value.args.length === 1 &&
122125
instr.value.args[0].kind === 'Identifier' &&
123-
instr.value.callee.loc !== GeneratedSource &&
124-
instr.value.callee.loc.identifierName !== undefined &&
125-
instr.value.callee.loc.identifierName !== null
126+
instr.value.callee.loc !== GeneratedSource
126127
) {
127-
setStateCalls.set(
128-
instr.value.callee.loc.identifierName,
129-
instr.value.callee,
130-
);
128+
if (setStateCalls.has(instr.value.callee.loc.identifierName)) {
129+
setStateCalls
130+
.get(instr.value.callee.loc.identifierName)!
131+
.push(instr.value.callee);
132+
} else {
133+
setStateCalls.set(instr.value.callee.loc.identifierName, [
134+
instr.value.callee,
135+
]);
136+
}
131137
}
132138

133139
let sources: DerivationMetadata[] = [];
@@ -245,11 +251,8 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
245251
const locals: Map<IdentifierId, IdentifierId> = new Map();
246252
const derivedTuple: Map<IdentifierId, DerivationMetadata> = new Map();
247253

248-
// Investigating
249-
const effectSetStates: Map<string, Place> = new Map();
250-
const setStateCalls: Map<string, Place> = new Map();
251-
252-
// let shouldCalculateInRender: boolean = true;
254+
const effectSetStates: Map<SetStateName, Place[]> = new Map();
255+
const setStateCalls: Map<SetStateName, Place[]> = new Map();
253256

254257
const errors: ErrorMetadata[] = [];
255258

@@ -342,13 +345,37 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
342345
}
343346
}
344347

345-
console.log('setStateCalls: ', setStateCalls);
346-
console.log('effectSetStates: ', effectSetStates);
347348
const throwableErrors = new CompilerError();
348349
for (const error of errors) {
350+
let reason;
351+
let description = '';
352+
// TODO: Not sure if this is robust enough.
353+
/*
354+
* If we use a setState from an invalid useEffect elsewhere then we probably have to
355+
* hoist state up, else we should calculate in render
356+
*/
357+
if (
358+
setStateCalls.get(error.setStateName)?.length !=
359+
effectSetStates.get(error.setStateName)?.length
360+
) {
361+
reason =
362+
'Consider lifting state up to the parent component to make this a controlled component. (https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes)';
363+
} else {
364+
reason =
365+
'You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)';
366+
}
367+
368+
if (error.propInfo !== undefined) {
369+
description += error.propInfo;
370+
}
371+
372+
if (error.localStateInfo !== undefined) {
373+
description += error.localStateInfo;
374+
}
375+
349376
throwableErrors.push({
350-
reason: `You may not need an effect. Values derived from state should be calculated in render, not in an effect. `,
351-
description: `You are using a value derived from props${error.propInfo} to update local state in an effect.`,
377+
reason: reason,
378+
description: description,
352379
severity: ErrorSeverity.InvalidReact,
353380
loc: error.loc,
354381
});
@@ -363,7 +390,7 @@ function validateEffect(
363390
effectFunction: HIRFunction,
364391
effectDeps: Array<IdentifierId>,
365392
derivedTuple: Map<IdentifierId, DerivationMetadata>,
366-
effectSetStates: Map<string, Place>,
393+
effectSetStates: Map<SetStateName, Place[]>,
367394
errors: ErrorMetadata[],
368395
): void {
369396
/*
@@ -437,10 +464,15 @@ function validateEffect(
437464
instr.value.callee.loc.identifierName !== undefined &&
438465
instr.value.callee.loc.identifierName !== null
439466
) {
440-
effectSetStates.set(
441-
instr.value.callee.loc.identifierName,
442-
instr.value.callee,
443-
);
467+
if (effectSetStates.has(instr.value.callee.loc.identifierName)) {
468+
effectSetStates
469+
.get(instr.value.callee.loc.identifierName)!
470+
.push(instr.value.callee);
471+
} else {
472+
effectSetStates.set(instr.value.callee.loc.identifierName, [
473+
instr.value.callee,
474+
]);
475+
}
444476
}
445477
switch (instr.value.kind) {
446478
case 'Primitive':
@@ -517,12 +549,6 @@ function validateEffect(
517549
seenBlocks.add(block.id);
518550
}
519551

520-
// need to track if the setState call has been used elsewhere
521-
// if it is then the solution should be to lift the state up to the parent component
522-
// if not the solution should be to calculate the value in rende
523-
//
524-
// If the same setState is used both inside and outside the effect
525-
526552
for (const call of setStateCallsInEffect) {
527553
if (call.invalidDeps != null) {
528554
let propNames = '';
@@ -538,15 +564,19 @@ function validateEffect(
538564
errors.push({
539565
errorType: 'HoistState',
540566
propInfo: propInfo,
567+
localStateInfo: undefined,
541568
loc: call.loc,
542-
setStateId: call.setStateId,
569+
setStateName:
570+
call.loc !== GeneratedSource ? call.loc.identifierName : undefined,
543571
});
544572
} else {
545573
errors.push({
546574
errorType: 'CalculateInRender',
547575
propInfo: undefined,
576+
localStateInfo: undefined,
548577
loc: call.loc,
549-
setStateId: call.setStateId,
578+
setStateName:
579+
call.loc !== GeneratedSource ? call.loc.identifierName : undefined,
550580
});
551581
}
552582
}

0 commit comments

Comments
 (0)