@@ -102,31 +102,24 @@ class DerivationCache {
102102 typeOfValue : typeOfValue ?? 'ignored' ,
103103 } ;
104104
105- if ( sourcesIds !== undefined ) {
106- for ( const id of sourcesIds ) {
107- const sourcePlace = this . cache . get ( id ) ?. place ;
105+ if ( isNamedIdentifier ( derivedVar ) ) {
106+ newValue . sourcesIds . add ( derivedVar . identifier . id ) ;
107+ }
108108
109- if ( sourcePlace === undefined ) {
110- continue ;
111- }
109+ for ( const id of sourcesIds ) {
110+ const sourceMetadata = this . cache . get ( id ) ;
112111
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- }
112+ if ( sourceMetadata === undefined ) {
113+ continue ;
125114 }
126- }
127115
128- if ( newValue . sourcesIds . size === 0 ) {
129- newValue . sourcesIds . add ( derivedVar . identifier . id ) ;
116+ if ( isNamedIdentifier ( sourceMetadata . place ) ) {
117+ newValue . sourcesIds . add ( sourceMetadata . place . identifier . id ) ;
118+ } else {
119+ for ( const sourcesSourceId of sourceMetadata . sourcesIds ) {
120+ newValue . sourcesIds . add ( sourcesSourceId ) ;
121+ }
122+ }
130123 }
131124
132125 this . cache . set ( derivedVar . identifier . id , newValue ) ;
@@ -151,6 +144,12 @@ class DerivationCache {
151144 }
152145}
153146
147+ function isNamedIdentifier ( place : Place ) : Boolean {
148+ return (
149+ place . identifier . name !== null && place . identifier . name ?. kind === 'named'
150+ ) ;
151+ }
152+
154153/**
155154 * Validates that useEffect is not used for derived computations which could/should
156155 * be performed in render.
@@ -202,7 +201,9 @@ export function validateNoDerivedComputationsInEffects_exp(
202201 if ( param . kind === 'Identifier' ) {
203202 context . derivationCache . cache . set ( param . identifier . id , {
204203 place : param ,
205- sourcesIds : new Set ( [ param . identifier . id ] ) ,
204+ sourcesIds : new Set (
205+ isNamedIdentifier ( param ) ? [ param . identifier . id ] : [ ] ,
206+ ) ,
206207 typeOfValue : 'fromProps' ,
207208 } ) ;
208209 }
@@ -212,7 +213,9 @@ export function validateNoDerivedComputationsInEffects_exp(
212213 if ( props != null && props . kind === 'Identifier' ) {
213214 context . derivationCache . cache . set ( props . identifier . id , {
214215 place : props ,
215- sourcesIds : new Set ( [ props . identifier . id ] ) ,
216+ sourcesIds : new Set (
217+ isNamedIdentifier ( props ) ? [ props . identifier . id ] : [ ] ,
218+ ) ,
216219 typeOfValue : 'fromProps' ,
217220 } ) ;
218221 }
@@ -223,10 +226,10 @@ export function validateNoDerivedComputationsInEffects_exp(
223226 context . derivationCache . takeSnapshot ( ) ;
224227
225228 for ( const block of fn . body . blocks . values ( ) ) {
229+ recordPhiDerivations ( block , context ) ;
226230 for ( const instr of block . instructions ) {
227231 recordInstructionDerivations ( instr , context , isFirstPass ) ;
228232 }
229- recordPhiDerivations ( block , context ) ;
230233 }
231234
232235 context . derivationCache . checkForChanges ( ) ;
@@ -237,6 +240,7 @@ export function validateNoDerivedComputationsInEffects_exp(
237240 validateEffect ( effect , context ) ;
238241 }
239242
243+ console . log ( derivationCache . cache ) ;
240244 if ( errors . hasAnyErrors ( ) ) {
241245 throw errors ;
242246 }
@@ -293,6 +297,7 @@ function recordInstructionDerivations(
293297 if ( value . kind === 'FunctionExpression' ) {
294298 context . functions . set ( lvalue . identifier . id , value ) ;
295299 for ( const [ , block ] of value . loweredFunc . func . body . blocks ) {
300+ recordPhiDerivations ( block , context ) ;
296301 for ( const instr of block . instructions ) {
297302 recordInstructionDerivations ( instr , context , isFirstPass ) ;
298303 }
@@ -341,9 +346,7 @@ function recordInstructionDerivations(
341346 }
342347
343348 typeOfValue = joinValue ( typeOfValue , operandMetadata . typeOfValue ) ;
344- for ( const id of operandMetadata . sourcesIds ) {
345- sources . add ( id ) ;
346- }
349+ sources . add ( operand . identifier . id ) ;
347350 }
348351
349352 if ( typeOfValue === 'ignored' ) {
@@ -406,6 +409,60 @@ function recordInstructionDerivations(
406409 }
407410}
408411
412+ function buildDataFlowTree (
413+ sourceId : IdentifierId ,
414+ indent : string = '' ,
415+ isLast : boolean = true ,
416+ context : ValidationContext ,
417+ ) : string {
418+ const sourceMetadata = context . derivationCache . cache . get ( sourceId ) ;
419+ if ( ! sourceMetadata || ! sourceMetadata . place . identifier . name ?. value ) {
420+ return '' ;
421+ }
422+
423+ const sourceName = sourceMetadata . place . identifier . name . value ;
424+ const prefix = indent + ( isLast ? '└── ' : '├── ' ) ;
425+ const childIndent = indent + ( isLast ? ' ' : '│ ' ) ;
426+
427+ const childSourceIds = Array . from ( sourceMetadata . sourcesIds ) . filter (
428+ id => id !== sourceId ,
429+ ) ;
430+
431+ const isOriginal = childSourceIds . length === 0 ;
432+
433+ let result = `${ prefix } ${ sourceName } ` ;
434+
435+ if ( isOriginal ) {
436+ let typeLabel : string ;
437+ if ( sourceMetadata . typeOfValue === 'fromProps' ) {
438+ typeLabel = 'Prop' ;
439+ } else if ( sourceMetadata . typeOfValue === 'fromState' ) {
440+ typeLabel = 'State' ;
441+ } else {
442+ typeLabel = 'Prop and State' ;
443+ }
444+ result += ` (${ typeLabel } )` ;
445+ }
446+
447+ if ( childSourceIds . length > 0 ) {
448+ result += '\n' ;
449+ childSourceIds . forEach ( ( childId , index ) => {
450+ const childTree = buildDataFlowTree (
451+ childId ,
452+ childIndent ,
453+ index === childSourceIds . length - 1 ,
454+ context ,
455+ ) ;
456+ if ( childTree ) {
457+ result += childTree + '\n' ;
458+ }
459+ } ) ;
460+ result = result . slice ( 0 , - 1 ) ;
461+ }
462+
463+ return result ;
464+ }
465+
409466function validateEffect (
410467 effectFunction : HIRFunction ,
411468 context : ValidationContext ,
@@ -508,27 +565,63 @@ function validateEffect(
508565 . length -
509566 1
510567 ) {
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 } ]` ;
568+ const allSourceIds = Array . from ( derivedSetStateCall . sourceIds ) ;
569+ const trees = allSourceIds
570+ . map ( ( id , index ) =>
571+ buildDataFlowTree ( id , '' , index === allSourceIds . length - 1 , context ) ,
572+ )
573+ . filter ( Boolean ) ;
574+
575+ const propsSet = new Set < string > ( ) ;
576+ const stateSet = new Set < string > ( ) ;
577+
578+ for ( const sourceId of derivedSetStateCall . sourceIds ) {
579+ const sourceMetadata = context . derivationCache . cache . get ( sourceId ) ;
580+ if (
581+ sourceMetadata &&
582+ sourceMetadata . place . identifier . name ?. value &&
583+ ( sourceMetadata . sourcesIds . size === 0 ||
584+ ( sourceMetadata . sourcesIds . size === 1 &&
585+ sourceMetadata . sourcesIds . has ( sourceId ) ) )
586+ ) {
587+ const name = sourceMetadata . place . identifier . name . value ;
588+ if ( sourceMetadata . typeOfValue === 'fromProps' ) {
589+ propsSet . add ( name ) ;
590+ } else if ( sourceMetadata . typeOfValue === 'fromState' ) {
591+ stateSet . add ( name ) ;
592+ } else if ( sourceMetadata . typeOfValue === 'fromPropsAndState' ) {
593+ propsSet . add ( name ) ;
594+ stateSet . add ( name ) ;
595+ }
596+ }
597+ }
598+
599+ const propsArr = Array . from ( propsSet ) ;
600+ const stateArr = Array . from ( stateSet ) ;
601+
602+ let rootSources = '' ;
603+ if ( propsArr . length > 0 ) {
604+ rootSources += `Props: [${ propsArr . join ( ', ' ) } ]` ;
527605 }
606+ if ( stateArr . length > 0 ) {
607+ if ( rootSources ) rootSources += '\n' ;
608+ rootSources += `State: [${ stateArr . join ( ', ' ) } ]` ;
609+ }
610+
611+ const description = `Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
612+
613+ This setState call is setting a derived value that depends on the following reactive sources:
614+
615+ ${ rootSources }
616+
617+ Data Flow Tree:
618+ ${ trees . join ( '\n' ) }
619+
620+ See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state` ;
528621
529622 context . errors . pushDiagnostic (
530623 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` ,
624+ description : description ,
532625 category : ErrorCategory . EffectDerivationsOfState ,
533626 reason :
534627 'You might not need an effect. Derive values in render, not effects.' ,
0 commit comments