|
1 | 1 | import { act, teardown as teardownAct } from 'preact/test-utils'; |
2 | | -import { createElement, render, Fragment, Component } from 'preact'; |
| 2 | +import { createElement, render, Fragment, Component, options } from 'preact'; |
3 | 3 | import { useEffect, useState, useRef } from 'preact/hooks'; |
4 | 4 | import { setupScratch, teardown } from '../../../test/_util/helpers'; |
5 | 5 | import { useEffectAssertions } from './useEffectAssertions'; |
@@ -665,4 +665,62 @@ describe('useEffect', () => { |
665 | 665 | setVal(1); |
666 | 666 | }); |
667 | 667 | }); |
| 668 | + |
| 669 | + it('should flush all pending effects when one component re-renders before afterPaint', () => { |
| 670 | + // Regression test: when only one component re-renders (via setState) |
| 671 | + // before afterPaint has flushed effects, ALL pending effects from |
| 672 | + // ALL components should be flushed as a batch before the re-render |
| 673 | + // proceeds. This matches React's flushPendingEffects() behavior. |
| 674 | + // |
| 675 | + // Without the fix, only the re-rendering component's stale effects |
| 676 | + // were flushed in _render, leaving sibling effects unflushed until |
| 677 | + // they got their own re-render or afterPaint fired. |
| 678 | + const order = []; |
| 679 | + let setB; |
| 680 | + |
| 681 | + function A() { |
| 682 | + useEffect(() => { |
| 683 | + order.push('A effect'); |
| 684 | + }, []); |
| 685 | + return <div>A</div>; |
| 686 | + } |
| 687 | + |
| 688 | + function B() { |
| 689 | + const [val, _setB] = useState(0); |
| 690 | + setB = _setB; |
| 691 | + useEffect(() => { |
| 692 | + order.push('B effect'); |
| 693 | + }, []); |
| 694 | + return <div>{val}</div>; |
| 695 | + } |
| 696 | + |
| 697 | + function App() { |
| 698 | + return ( |
| 699 | + <div> |
| 700 | + <A /> |
| 701 | + <B /> |
| 702 | + </div> |
| 703 | + ); |
| 704 | + } |
| 705 | + |
| 706 | + render(<App />, scratch); |
| 707 | + |
| 708 | + // Effects are queued but not yet flushed (afterPaint hasn't fired). |
| 709 | + expect(order).to.deep.equal([]); |
| 710 | + |
| 711 | + // Force B to re-render synchronously by overriding debounceRendering. |
| 712 | + // This simulates what happens when a store subscription or |
| 713 | + // useLayoutEffect triggers a re-render before afterPaint. |
| 714 | + const prev = options.debounceRendering; |
| 715 | + options.debounceRendering = cb => cb(); |
| 716 | + setB(1); |
| 717 | + options.debounceRendering = prev; |
| 718 | + |
| 719 | + // With the fix: B's _render detects stale effects and calls |
| 720 | + // flushAfterPaintEffects(), which flushes BOTH A's and B's effects. |
| 721 | + // Without the fix: only B's effects would flush here; A's effect |
| 722 | + // would be deferred. |
| 723 | + expect(order).to.include('A effect'); |
| 724 | + expect(order).to.include('B effect'); |
| 725 | + }); |
668 | 726 | }); |
0 commit comments