Skip to content

Commit 4ddf4a5

Browse files
committed
Transfer mounted effects on suspend in legacy mode
In legacy mode, a component that suspends bails out and commit in its previous state. If the component previously had mounted effects, we must transfer those to the work-in-progress so they don't get dropped.
1 parent 8dea154 commit 4ddf4a5

File tree

2 files changed

+49
-0
lines changed

2 files changed

+49
-0
lines changed

packages/react-reconciler/src/ReactFiberThrow.js

+2
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,11 @@ function throwException(
199199
// to render it.
200200
let currentSource = sourceFiber.alternate;
201201
if (currentSource) {
202+
sourceFiber.updateQueue = currentSource.updateQueue;
202203
sourceFiber.memoizedState = currentSource.memoizedState;
203204
sourceFiber.expirationTime = currentSource.expirationTime;
204205
} else {
206+
sourceFiber.updateQueue = null;
205207
sourceFiber.memoizedState = null;
206208
}
207209
}

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

+47
Original file line numberDiff line numberDiff line change
@@ -1448,6 +1448,53 @@ function loadModules({
14481448
'Caught an error: Error in host config.',
14491449
);
14501450
});
1451+
1452+
it('does not drop mounted effects', async () => {
1453+
let never = {then() {}};
1454+
1455+
let setShouldSuspend;
1456+
function App() {
1457+
const [shouldSuspend, _setShouldSuspend] = React.useState(0);
1458+
setShouldSuspend = _setShouldSuspend;
1459+
return (
1460+
<Suspense fallback="Loading...">
1461+
<Child shouldSuspend={shouldSuspend} />
1462+
</Suspense>
1463+
);
1464+
}
1465+
1466+
function Child({shouldSuspend}) {
1467+
if (shouldSuspend === 1) {
1468+
throw never;
1469+
}
1470+
1471+
React.useEffect(() => {
1472+
Scheduler.unstable_yieldValue('Mount');
1473+
return () => {
1474+
Scheduler.unstable_yieldValue('Unmount');
1475+
};
1476+
}, []);
1477+
1478+
return 'Child';
1479+
}
1480+
1481+
const root = ReactNoop.createLegacyRoot(null);
1482+
await ReactNoop.act(async () => {
1483+
root.render(<App />);
1484+
});
1485+
expect(Scheduler).toHaveYielded(['Mount']);
1486+
1487+
// Suspend the child. This puts it into an inconsistent state.
1488+
await ReactNoop.act(async () => {
1489+
setShouldSuspend(true);
1490+
});
1491+
1492+
// Unmount everying
1493+
await ReactNoop.act(async () => {
1494+
root.render(null);
1495+
});
1496+
expect(Scheduler).toHaveYielded(['Unmount']);
1497+
});
14511498
});
14521499

14531500
it('does not call lifecycles of a suspended component', async () => {

0 commit comments

Comments
 (0)