@@ -19,8 +19,9 @@ import {
1919 Instruction ,
2020 isUseStateType ,
2121 isUseRefType ,
22+ GeneratedSource ,
23+ SourceLocation ,
2224} from '../HIR' ;
23- import { printInstruction } from '../HIR/PrintHIR' ;
2425import { eachInstructionLValue , eachInstructionOperand } from '../HIR/visitors' ;
2526import { isMutable } from '../ReactiveScopes/InferReactiveScopeVariables' ;
2627import { assertExhaustive } from '../Utils/utils' ;
@@ -60,6 +61,10 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
6061 const functions : Map < IdentifierId , FunctionExpression > = new Map ( ) ;
6162
6263 const derivationCache : Map < IdentifierId , DerivationMetadata > = new Map ( ) ;
64+ const setStateCache : Map < string | undefined | null , Array < Place > > = new Map ( ) ;
65+
66+ const effects : Array < HIRFunction > = [ ] ;
67+
6368 if ( fn . fnType === 'Hook' ) {
6469 for ( const param of fn . params ) {
6570 if ( param . kind === 'Identifier' ) {
@@ -128,11 +133,7 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
128133 ) {
129134 const effectFunction = functions . get ( value . args [ 0 ] . identifier . id ) ;
130135 if ( effectFunction != null ) {
131- validateEffect (
132- effectFunction . loweredFunc . func ,
133- errors ,
134- derivationCache ,
135- ) ;
136+ effects . push ( effectFunction . loweredFunc . func ) ;
136137 }
137138 } else if ( isUseStateType ( lvalue . identifier ) ) {
138139 const stateValueSource = value . args [ 0 ] ;
@@ -144,6 +145,25 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
144145 }
145146
146147 for ( const operand of eachInstructionOperand ( instr ) ) {
148+ // Record setState usages everywhere
149+ switch ( instr . value . kind ) {
150+ case 'JsxExpression' :
151+ case 'CallExpression' :
152+ case 'MethodCall' :
153+ if (
154+ isSetStateType ( operand . identifier ) &&
155+ operand . loc !== GeneratedSource
156+ ) {
157+ if ( setStateCache . has ( operand . loc . identifierName ) ) {
158+ setStateCache . get ( operand . loc . identifierName ) ! . push ( operand ) ;
159+ } else {
160+ setStateCache . set ( operand . loc . identifierName , [ operand ] ) ;
161+ }
162+ }
163+ break ;
164+ default :
165+ }
166+
147167 const operandMetadata = derivationCache . get ( operand . identifier . id ) ;
148168
149169 if ( operandMetadata === undefined ) {
@@ -212,6 +232,10 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
212232 }
213233 }
214234
235+ for ( const effect of effects ) {
236+ validateEffect ( effect , errors , derivationCache , setStateCache ) ;
237+ }
238+
215239 if ( errors . hasAnyErrors ( ) ) {
216240 throw errors ;
217241 }
@@ -269,11 +293,17 @@ function validateEffect(
269293 effectFunction : HIRFunction ,
270294 errors : CompilerError ,
271295 derivationCache : Map < IdentifierId , DerivationMetadata > ,
296+ setStateCache : Map < string | undefined | null , Array < Place > > ,
272297) : void {
298+ const effectSetStateCache : Map <
299+ string | undefined | null ,
300+ Array < Place >
301+ > = new Map ( ) ;
273302 const seenBlocks : Set < BlockId > = new Set ( ) ;
274303
275304 const effectDerivedSetStateCalls : Array < {
276305 value : CallExpression ;
306+ loc : SourceLocation ;
277307 sourceIds : Set < IdentifierId > ;
278308 } > = [ ] ;
279309
@@ -292,6 +322,28 @@ function validateEffect(
292322 return ;
293323 }
294324
325+ for ( const operand of eachInstructionOperand ( instr ) ) {
326+ switch ( instr . value . kind ) {
327+ case 'JsxExpression' :
328+ case 'CallExpression' :
329+ case 'MethodCall' :
330+ if (
331+ isSetStateType ( operand . identifier ) &&
332+ operand . loc !== GeneratedSource
333+ ) {
334+ if ( effectSetStateCache . has ( operand . loc . identifierName ) ) {
335+ effectSetStateCache
336+ . get ( operand . loc . identifierName ) !
337+ . push ( operand ) ;
338+ } else {
339+ effectSetStateCache . set ( operand . loc . identifierName , [ operand ] ) ;
340+ }
341+ }
342+ break ;
343+ default :
344+ }
345+ }
346+
295347 if (
296348 instr . value . kind === 'CallExpression' &&
297349 isSetStateType ( instr . value . callee . identifier ) &&
@@ -305,6 +357,7 @@ function validateEffect(
305357 if ( argMetadata !== undefined ) {
306358 effectDerivedSetStateCalls . push ( {
307359 value : instr . value ,
360+ loc : instr . value . callee . loc ,
308361 sourceIds : argMetadata . sourcesIds ,
309362 } ) ;
310363 }
@@ -337,13 +390,22 @@ function validateEffect(
337390 }
338391
339392 for ( const derivedSetStateCall of effectDerivedSetStateCalls ) {
340- errors . push ( {
341- category : ErrorCategory . EffectDerivationsOfState ,
342- reason :
343- 'Values derived from props and 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)' ,
344- description : null ,
345- loc : derivedSetStateCall . value . callee . loc ,
346- suggestions : null ,
347- } ) ;
393+ if (
394+ derivedSetStateCall . loc !== GeneratedSource &&
395+ effectSetStateCache . has ( derivedSetStateCall . loc . identifierName ) &&
396+ setStateCache . has ( derivedSetStateCall . loc . identifierName ) &&
397+ effectSetStateCache . get ( derivedSetStateCall . loc . identifierName ) !
398+ . length ===
399+ setStateCache . get ( derivedSetStateCall . loc . identifierName ) ! . length
400+ ) {
401+ errors . push ( {
402+ category : ErrorCategory . EffectDerivationsOfState ,
403+ reason :
404+ 'Values derived from props and 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)' ,
405+ description : null ,
406+ loc : derivedSetStateCall . value . callee . loc ,
407+ suggestions : null ,
408+ } ) ;
409+ }
348410 }
349411}
0 commit comments