Skip to content

Commit 82cf50a

Browse files
committed
Bugfix: Dropped effects in Legacy Mode Suspense (facebook#18238)
* Failing: Dropped effects in Legacy Mode Suspense * 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 d28bd29 commit 82cf50a

File tree

3 files changed

+77
-0
lines changed

3 files changed

+77
-0
lines changed

packages/react-noop-renderer/src/createReactNoop.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,32 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
785785
};
786786
},
787787

788+
createLegacyRoot() {
789+
const container = {
790+
rootID: '' + idCounter++,
791+
pendingChildren: [],
792+
children: [],
793+
};
794+
const fiberRoot = NoopRenderer.createContainer(
795+
container,
796+
LegacyRoot,
797+
false,
798+
null,
799+
);
800+
return {
801+
_Scheduler: Scheduler,
802+
render(children: ReactNodeList) {
803+
NoopRenderer.updateContainer(children, fiberRoot, null, null);
804+
},
805+
getChildren() {
806+
return getChildren(container);
807+
},
808+
getChildrenAsJSX() {
809+
return getChildrenAsJSX(container);
810+
},
811+
};
812+
},
813+
788814
getChildrenAsJSX(rootID: string = DEFAULT_ROOT_ID) {
789815
const container = rootContainers.get(rootID);
790816
return getChildrenAsJSX(container);

packages/react-reconciler/src/ReactFiberThrow.js

Lines changed: 2 additions & 0 deletions
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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1467,6 +1467,55 @@ function loadModules({
14671467
'Caught an error: Error in host config.',
14681468
);
14691469
});
1470+
1471+
it('does not drop mounted effects', async () => {
1472+
let never = {then() {}};
1473+
1474+
let setShouldSuspend;
1475+
function App() {
1476+
const [shouldSuspend, _setShouldSuspend] = React.useState(0);
1477+
setShouldSuspend = _setShouldSuspend;
1478+
return (
1479+
<Suspense fallback="Loading...">
1480+
<Child shouldSuspend={shouldSuspend} />
1481+
</Suspense>
1482+
);
1483+
}
1484+
1485+
function Child({shouldSuspend}) {
1486+
if (shouldSuspend) {
1487+
throw never;
1488+
}
1489+
1490+
React.useEffect(() => {
1491+
Scheduler.unstable_yieldValue('Mount');
1492+
return () => {
1493+
Scheduler.unstable_yieldValue('Unmount');
1494+
};
1495+
}, []);
1496+
1497+
return 'Child';
1498+
}
1499+
1500+
const root = ReactNoop.createLegacyRoot(null);
1501+
await ReactNoop.act(async () => {
1502+
root.render(<App />);
1503+
});
1504+
expect(Scheduler).toHaveYielded(['Mount']);
1505+
expect(root).toMatchRenderedOutput('Child');
1506+
1507+
// Suspend the child. This puts it into an inconsistent state.
1508+
await ReactNoop.act(async () => {
1509+
setShouldSuspend(true);
1510+
});
1511+
expect(root).toMatchRenderedOutput('Loading...');
1512+
1513+
// Unmount everying
1514+
await ReactNoop.act(async () => {
1515+
root.render(null);
1516+
});
1517+
expect(Scheduler).toHaveYielded(['Unmount']);
1518+
});
14701519
});
14711520

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

0 commit comments

Comments
 (0)