Skip to content

Commit 4f3f7ee

Browse files
authored
Bugfix: Effect clean up when deleting suspended tree (#19752)
* Bug: Effect clean up when deleting suspended tree Adds a failing unit test. * Re-use static flags from suspended primary tree When switching to a Suspense boundary's fallback, we need to be sure to preserve static subtree flags from the primary tree.
1 parent 7baf9d4 commit 4f3f7ee

File tree

2 files changed

+104
-0
lines changed

2 files changed

+104
-0
lines changed

packages/react-reconciler/src/ReactFiberBeginWork.new.js

+7
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import {
6464
Ref,
6565
Deletion,
6666
ForceUpdateForLegacySuspense,
67+
StaticMask,
6768
} from './ReactFiberFlags';
6869
import ReactSharedInternals from 'shared/ReactSharedInternals';
6970
import {
@@ -2131,6 +2132,12 @@ function updateSuspenseFallbackChildren(
21312132
currentPrimaryChildFragment,
21322133
primaryChildProps,
21332134
);
2135+
2136+
// Since we're reusing a current tree, we need to reuse the flags, too.
2137+
// (We don't do this in legacy mode, because in legacy mode we don't re-use
2138+
// the current tree; see previous branch.)
2139+
primaryChildFragment.subtreeFlags =
2140+
currentPrimaryChildFragment.subtreeFlags & StaticMask;
21342141
}
21352142
let fallbackChildFragment;
21362143
if (currentFallbackChildFragment !== null) {

packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js

+97
Original file line numberDiff line numberDiff line change
@@ -3956,4 +3956,101 @@ describe('ReactSuspenseWithNoopRenderer', () => {
39563956
</>,
39573957
);
39583958
});
3959+
3960+
it('should fire effect clean-up when deleting suspended tree', async () => {
3961+
const {useEffect} = React;
3962+
3963+
function App({show}) {
3964+
return (
3965+
<Suspense fallback={<Text text="Loading..." />}>
3966+
<Child />
3967+
{show && <AsyncText text="Async" />}
3968+
</Suspense>
3969+
);
3970+
}
3971+
3972+
function Child() {
3973+
useEffect(() => {
3974+
Scheduler.unstable_yieldValue('Mount Child');
3975+
return () => {
3976+
Scheduler.unstable_yieldValue('Unmount Child');
3977+
};
3978+
}, []);
3979+
return <span prop="Child" />;
3980+
}
3981+
3982+
const root = ReactNoop.createRoot();
3983+
3984+
await ReactNoop.act(async () => {
3985+
root.render(<App show={false} />);
3986+
});
3987+
expect(Scheduler).toHaveYielded(['Mount Child']);
3988+
expect(root).toMatchRenderedOutput(<span prop="Child" />);
3989+
3990+
await ReactNoop.act(async () => {
3991+
root.render(<App show={true} />);
3992+
});
3993+
// TODO: `act` should have already flushed the placeholder, so this
3994+
// runAllTimers call should be unnecessary.
3995+
jest.runAllTimers();
3996+
expect(Scheduler).toHaveYielded(['Suspend! [Async]', 'Loading...']);
3997+
expect(root).toMatchRenderedOutput(
3998+
<>
3999+
<span hidden={true} prop="Child" />
4000+
<span prop="Loading..." />
4001+
</>,
4002+
);
4003+
4004+
await ReactNoop.act(async () => {
4005+
root.render(null);
4006+
});
4007+
expect(Scheduler).toHaveYielded(['Unmount Child']);
4008+
});
4009+
4010+
it('should fire effect clean-up when deleting suspended tree (legacy)', async () => {
4011+
const {useEffect} = React;
4012+
4013+
function App({show}) {
4014+
return (
4015+
<Suspense fallback={<Text text="Loading..." />}>
4016+
<Child />
4017+
{show && <AsyncText text="Async" />}
4018+
</Suspense>
4019+
);
4020+
}
4021+
4022+
function Child() {
4023+
useEffect(() => {
4024+
Scheduler.unstable_yieldValue('Mount Child');
4025+
return () => {
4026+
Scheduler.unstable_yieldValue('Unmount Child');
4027+
};
4028+
}, []);
4029+
return <span prop="Child" />;
4030+
}
4031+
4032+
const root = ReactNoop.createLegacyRoot();
4033+
4034+
await ReactNoop.act(async () => {
4035+
root.render(<App show={false} />);
4036+
});
4037+
expect(Scheduler).toHaveYielded(['Mount Child']);
4038+
expect(root).toMatchRenderedOutput(<span prop="Child" />);
4039+
4040+
await ReactNoop.act(async () => {
4041+
root.render(<App show={true} />);
4042+
});
4043+
expect(Scheduler).toHaveYielded(['Suspend! [Async]', 'Loading...']);
4044+
expect(root).toMatchRenderedOutput(
4045+
<>
4046+
<span hidden={true} prop="Child" />
4047+
<span prop="Loading..." />
4048+
</>,
4049+
);
4050+
4051+
await ReactNoop.act(async () => {
4052+
root.render(null);
4053+
});
4054+
expect(Scheduler).toHaveYielded(['Unmount Child']);
4055+
});
39594056
});

0 commit comments

Comments
 (0)