@@ -23,6 +23,7 @@ import {
2323 GeneratedSource ,
2424 SourceLocation ,
2525} from '../HIR' ;
26+ import { printInstruction } from '../HIR/PrintHIR' ;
2627import { eachInstructionLValue , eachInstructionOperand } from '../HIR/visitors' ;
2728import { isMutable } from '../ReactiveScopes/InferReactiveScopeVariables' ;
2829import { assertExhaustive } from '../Utils/utils' ;
@@ -102,31 +103,24 @@ class DerivationCache {
102103 typeOfValue : typeOfValue ?? 'ignored' ,
103104 } ;
104105
105- if ( sourcesIds !== undefined ) {
106- for ( const id of sourcesIds ) {
107- const sourcePlace = this . cache . get ( id ) ?. place ;
106+ if ( isNamedIdentifier ( derivedVar ) ) {
107+ newValue . sourcesIds . add ( derivedVar . identifier . id ) ;
108+ }
108109
109- if ( sourcePlace === undefined ) {
110- continue ;
111- }
110+ for ( const id of sourcesIds ) {
111+ const sourceMetadata = this . cache . get ( id ) ;
112112
113- /*
114- * If the identifier of the source is a promoted identifier, then
115- * we should set the target as the source.
116- */
117- if (
118- sourcePlace . identifier . name === null ||
119- sourcePlace . identifier . name ?. kind === 'promoted'
120- ) {
121- newValue . sourcesIds . add ( derivedVar . identifier . id ) ;
122- } else {
123- newValue . sourcesIds . add ( sourcePlace . identifier . id ) ;
124- }
113+ if ( sourceMetadata === undefined ) {
114+ continue ;
125115 }
126- }
127116
128- if ( newValue . sourcesIds . size === 0 ) {
129- newValue . sourcesIds . add ( derivedVar . identifier . id ) ;
117+ if ( isNamedIdentifier ( sourceMetadata . place ) ) {
118+ newValue . sourcesIds . add ( sourceMetadata . place . identifier . id ) ;
119+ } else {
120+ for ( const sourcesSourceId of sourceMetadata . sourcesIds ) {
121+ newValue . sourcesIds . add ( sourcesSourceId ) ;
122+ }
123+ }
130124 }
131125
132126 this . cache . set ( derivedVar . identifier . id , newValue ) ;
@@ -151,6 +145,12 @@ class DerivationCache {
151145 }
152146}
153147
148+ function isNamedIdentifier ( place : Place ) : Boolean {
149+ return (
150+ place . identifier . name !== null && place . identifier . name ?. kind === 'named'
151+ ) ;
152+ }
153+
154154/**
155155 * Validates that useEffect is not used for derived computations which could/should
156156 * be performed in render.
@@ -202,7 +202,9 @@ export function validateNoDerivedComputationsInEffects_exp(
202202 if ( param . kind === 'Identifier' ) {
203203 context . derivationCache . cache . set ( param . identifier . id , {
204204 place : param ,
205- sourcesIds : new Set ( [ param . identifier . id ] ) ,
205+ sourcesIds : new Set (
206+ isNamedIdentifier ( param ) ? [ param . identifier . id ] : [ ] ,
207+ ) ,
206208 typeOfValue : 'fromProps' ,
207209 } ) ;
208210 }
@@ -212,7 +214,9 @@ export function validateNoDerivedComputationsInEffects_exp(
212214 if ( props != null && props . kind === 'Identifier' ) {
213215 context . derivationCache . cache . set ( props . identifier . id , {
214216 place : props ,
215- sourcesIds : new Set ( [ props . identifier . id ] ) ,
217+ sourcesIds : new Set (
218+ isNamedIdentifier ( props ) ? [ props . identifier . id ] : [ ] ,
219+ ) ,
216220 typeOfValue : 'fromProps' ,
217221 } ) ;
218222 }
@@ -223,10 +227,10 @@ export function validateNoDerivedComputationsInEffects_exp(
223227 context . derivationCache . takeSnapshot ( ) ;
224228
225229 for ( const block of fn . body . blocks . values ( ) ) {
230+ recordPhiDerivations ( block , context ) ;
226231 for ( const instr of block . instructions ) {
227232 recordInstructionDerivations ( instr , context , isFirstPass ) ;
228233 }
229- recordPhiDerivations ( block , context ) ;
230234 }
231235
232236 context . derivationCache . checkForChanges ( ) ;
@@ -237,6 +241,7 @@ export function validateNoDerivedComputationsInEffects_exp(
237241 validateEffect ( effect , context ) ;
238242 }
239243
244+ console . log ( derivationCache . cache ) ;
240245 if ( errors . hasAnyErrors ( ) ) {
241246 throw errors ;
242247 }
@@ -293,6 +298,7 @@ function recordInstructionDerivations(
293298 if ( value . kind === 'FunctionExpression' ) {
294299 context . functions . set ( lvalue . identifier . id , value ) ;
295300 for ( const [ , block ] of value . loweredFunc . func . body . blocks ) {
301+ recordPhiDerivations ( block , context ) ;
296302 for ( const instr of block . instructions ) {
297303 recordInstructionDerivations ( instr , context , isFirstPass ) ;
298304 }
@@ -341,9 +347,7 @@ function recordInstructionDerivations(
341347 }
342348
343349 typeOfValue = joinValue ( typeOfValue , operandMetadata . typeOfValue ) ;
344- for ( const id of operandMetadata . sourcesIds ) {
345- sources . add ( id ) ;
346- }
350+ sources . add ( operand . identifier . id ) ;
347351 }
348352
349353 if ( typeOfValue === 'ignored' ) {
@@ -406,6 +410,60 @@ function recordInstructionDerivations(
406410 }
407411}
408412
413+ function buildDataFlowTree (
414+ sourceId : IdentifierId ,
415+ indent : string = '' ,
416+ isLast : boolean = true ,
417+ context : ValidationContext ,
418+ ) : string {
419+ const sourceMetadata = context . derivationCache . cache . get ( sourceId ) ;
420+ if ( ! sourceMetadata || ! sourceMetadata . place . identifier . name ?. value ) {
421+ return '' ;
422+ }
423+
424+ const sourceName = sourceMetadata . place . identifier . name . value ;
425+ const prefix = indent + ( isLast ? '└── ' : '├── ' ) ;
426+ const childIndent = indent + ( isLast ? ' ' : '│ ' ) ;
427+
428+ const childSourceIds = Array . from ( sourceMetadata . sourcesIds ) . filter (
429+ id => id !== sourceId ,
430+ ) ;
431+
432+ const isOriginal = childSourceIds . length === 0 ;
433+
434+ let result = `${ prefix } ${ sourceName } ` ;
435+
436+ if ( isOriginal ) {
437+ let typeLabel : string ;
438+ if ( sourceMetadata . typeOfValue === 'fromProps' ) {
439+ typeLabel = 'Prop' ;
440+ } else if ( sourceMetadata . typeOfValue === 'fromState' ) {
441+ typeLabel = 'State' ;
442+ } else {
443+ typeLabel = 'Prop and State' ;
444+ }
445+ result += ` (${ typeLabel } )` ;
446+ }
447+
448+ if ( childSourceIds . length > 0 ) {
449+ result += '\n' ;
450+ childSourceIds . forEach ( ( childId , index ) => {
451+ const childTree = buildDataFlowTree (
452+ childId ,
453+ childIndent ,
454+ index === childSourceIds . length - 1 ,
455+ context ,
456+ ) ;
457+ if ( childTree ) {
458+ result += childTree + '\n' ;
459+ }
460+ } ) ;
461+ result = result . slice ( 0 , - 1 ) ;
462+ }
463+
464+ return result ;
465+ }
466+
409467function validateEffect (
410468 effectFunction : HIRFunction ,
411469 context : ValidationContext ,
@@ -508,34 +566,68 @@ function validateEffect(
508566 . length -
509567 1
510568 ) {
511- const derivedDepsStr = Array . from ( derivedSetStateCall . sourceIds )
512- . map ( sourceId => {
513- const sourceMetadata = context . derivationCache . cache . get ( sourceId ) ;
514- return sourceMetadata ?. place . identifier . name ?. value ;
515- } )
516- . filter ( Boolean )
517- . join ( ', ' ) ;
518-
519- let description ;
520-
521- if ( derivedSetStateCall . typeOfValue === 'fromProps' ) {
522- description = `From props: [${ derivedDepsStr } ]` ;
523- } else if ( derivedSetStateCall . typeOfValue === 'fromState' ) {
524- description = `From local state: [${ derivedDepsStr } ]` ;
525- } else {
526- description = `From props and local state: [${ derivedDepsStr } ]` ;
569+ const allSourceIds = Array . from ( derivedSetStateCall . sourceIds ) ;
570+ const trees = allSourceIds
571+ . map ( ( id , index ) =>
572+ buildDataFlowTree ( id , '' , index === allSourceIds . length - 1 , context ) ,
573+ )
574+ . filter ( Boolean ) ;
575+
576+ const propsSet = new Set < string > ( ) ;
577+ const stateSet = new Set < string > ( ) ;
578+
579+ for ( const sourceId of derivedSetStateCall . sourceIds ) {
580+ const sourceMetadata = context . derivationCache . cache . get ( sourceId ) ;
581+ if (
582+ sourceMetadata &&
583+ sourceMetadata . place . identifier . name ?. value &&
584+ ( sourceMetadata . sourcesIds . size === 0 ||
585+ ( sourceMetadata . sourcesIds . size === 1 &&
586+ sourceMetadata . sourcesIds . has ( sourceId ) ) )
587+ ) {
588+ const name = sourceMetadata . place . identifier . name . value ;
589+ if ( sourceMetadata . typeOfValue === 'fromProps' ) {
590+ propsSet . add ( name ) ;
591+ } else if ( sourceMetadata . typeOfValue === 'fromState' ) {
592+ stateSet . add ( name ) ;
593+ } else if ( sourceMetadata . typeOfValue === 'fromPropsAndState' ) {
594+ propsSet . add ( name ) ;
595+ stateSet . add ( name ) ;
596+ }
597+ }
598+ }
599+
600+ const propsArr = Array . from ( propsSet ) ;
601+ const stateArr = Array . from ( stateSet ) ;
602+
603+ let rootSources = '' ;
604+ if ( propsArr . length > 0 ) {
605+ rootSources += `Props: [${ propsArr . join ( ', ' ) } ]` ;
527606 }
607+ if ( stateArr . length > 0 ) {
608+ if ( rootSources ) rootSources += '\n' ;
609+ rootSources += `State: [${ stateArr . join ( ', ' ) } ]` ;
610+ }
611+
612+ const description = `This setState call is setting a derived value that depends on the following reactive sources:
613+
614+ ${ rootSources }
615+
616+ Data Flow Tree:
617+ ${ trees . join ( '\n' ) }
618+
619+ See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state` ;
528620
529621 context . errors . pushDiagnostic (
530622 CompilerDiagnostic . create ( {
531- description : `Derived values ( ${ description } ) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user` ,
623+ description : description ,
532624 category : ErrorCategory . EffectDerivationsOfState ,
533625 reason :
534- 'You might not need an effect. Derive values in render, not effects .' ,
626+ 'Avoid derived computations in effects. Compute values during render instead .' ,
535627 } ) . withDetails ( {
536628 kind : 'error' ,
537629 loc : derivedSetStateCall . value . callee . loc ,
538- message : 'This should be computed during render, not in an effect ' ,
630+ message : 'Move this computation to render' ,
539631 } ) ,
540632 ) ;
541633 }
0 commit comments