1+ import { Component , Fragment , createElement , render } from 'preact' ;
2+ import { useEffect , useRef , useState } from 'preact/hooks' ;
13import { act , teardown as teardownAct } from 'preact/test-utils' ;
2- import { createElement , render , Fragment , Component } from 'preact' ;
3- import { useEffect , useState , useRef } from 'preact/hooks' ;
4+ import { vi } from 'vitest' ;
45import { setupScratch , teardown } from '../../../test/_util/helpers' ;
5- import { useEffectAssertions } from './useEffectAssertions' ;
66import { scheduleEffectAssert } from '../_util/useEffectUtil' ;
7- import { vi } from 'vitest ' ;
7+ import { useEffectAssertions } from './useEffectAssertions ' ;
88
99describe ( 'useEffect' , ( ) => {
1010 /** @type {HTMLDivElement } */
@@ -84,7 +84,7 @@ describe('useEffect', () => {
8484 } ) ;
8585
8686 it ( 'should execute effects in parent if child throws in effect' , async ( ) => {
87- let executionOrder = [ ] ;
87+ const executionOrder = [ ] ;
8888
8989 const Child = ( ) => {
9090 useEffect ( ( ) => {
@@ -268,8 +268,8 @@ describe('useEffect', () => {
268268 scratch . appendChild ( global ) ;
269269
270270 const Modal = props => {
271- let [ , setCanProceed ] = useState ( true ) ;
272- let ChildProp = props . content ;
271+ const [ , setCanProceed ] = useState ( true ) ;
272+ const ChildProp = props . content ;
273273
274274 return (
275275 < div >
@@ -425,9 +425,11 @@ describe('useEffect', () => {
425425 }
426426
427427 render ( ) {
428- return this . state . error
429- ? < h2 > Error! { this . state . error } </ h2 >
430- : this . props . children ;
428+ return this . state . error ? (
429+ < h2 > Error! { this . state . error } </ h2 >
430+ ) : (
431+ this . props . children
432+ ) ;
431433 }
432434 }
433435
@@ -634,6 +636,35 @@ describe('useEffect', () => {
634636 expect ( calls ) . to . deep . equal ( [ 'doing effecthi' ] ) ;
635637 } ) ;
636638
639+ it ( 'should not crash when effect throws and component is unmounted by render(null) during flush' , ( ) => {
640+ // In flushAfterPaintEffects():
641+ // 1. Guard checks component.__hooks — truthy, passes
642+ // 2. invokeEffect runs the effect callback
643+ // 3. The callback calls render(null, scratch) which unmounts the tree
644+ // → options.unmount sets component.__hooks = undefined
645+ // 4. Resetting the hooks array to an empty array would throw an error
646+ let setVal ;
647+
648+ function App ( ) {
649+ const [ val , _setVal ] = useState ( 0 ) ;
650+ setVal = _setVal ;
651+ useEffect ( ( ) => {
652+ if ( val === 1 ) {
653+ render ( null , scratch ) ;
654+ }
655+ } , [ val ] ) ;
656+ return < div > val: { val } </ div > ;
657+ }
658+
659+ act ( ( ) => {
660+ render ( < App /> , scratch ) ;
661+ } ) ;
662+
663+ act ( ( ) => {
664+ setVal ( 1 ) ;
665+ } ) ;
666+ } ) ;
667+
637668 it ( 'should not rerun when receiving NaN on subsequent renders' , ( ) => {
638669 const calls = [ ] ;
639670 const Component = ( { value } ) => {
@@ -647,7 +678,7 @@ describe('useEffect', () => {
647678 } , [ value ] ) ;
648679 return < p > { count } </ p > ;
649680 } ;
650- const App = ( ) => < Component value = { NaN } /> ;
681+ const App = ( ) => < Component value = { Number . NaN } /> ;
651682
652683 act ( ( ) => {
653684 render ( < App /> , scratch ) ;
0 commit comments