Bugfix: Dropped effects in Legacy Mode Suspense (#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.
Andrew Clark committed
Mar 6, 2020 at 11:10 UTC
235a6c4af67e3e1fbfab7088c857265e0c95b81f
4 files changed
+78
packages/react-noop-renderer/src/ReactNoop.js
+1
@@ -24,6 +24,7 @@ export const {
24
getOrCreateRootContainer,
25
createRoot,
26
createBlockingRoot,
27
+ createLegacyRoot,
28
getChildrenAsJSX,
29
getPendingChildrenAsJSX,
30
createPortal,
packages/react-noop-renderer/src/createReactNoop.js
+26
@@ -785,6 +785,32 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
785
};
786
},
787
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
+
814
getChildrenAsJSX(rootID: string = DEFAULT_ROOT_ID) {
815
const container = rootContainers.get(rootID);
816
return getChildrenAsJSX(container);
packages/react-reconciler/src/ReactFiberThrow.js
+2
@@ -199,9 +199,11 @@ function throwException(
199
// to render it.
200
let currentSource = sourceFiber.alternate;
201
if (currentSource) {
202
+ sourceFiber.updateQueue = currentSource.updateQueue;
203
sourceFiber.memoizedState = currentSource.memoizedState;
204
sourceFiber.expirationTime = currentSource.expirationTime;
205
} else {
206
+ sourceFiber.updateQueue = null;
207
sourceFiber.memoizedState = null;
208
}
209
}
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
+49
@@ -1448,6 +1448,55 @@ function loadModules({
1448
'Caught an error: Error in host config.',
1449
);
1450
});
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) {
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
+ expect(root).toMatchRenderedOutput('Child');
1487
+
1488
+ // Suspend the child. This puts it into an inconsistent state.
1489
+ await ReactNoop.act(async () => {
1490
+ setShouldSuspend(true);
1491
+ });
1492
+ expect(root).toMatchRenderedOutput('Loading...');
1493
+
1494
+ // Unmount everying
1495
+ await ReactNoop.act(async () => {
1496
+ root.render(null);
1497
+ });
1498
+ expect(Scheduler).toHaveYielded(['Unmount']);
1499
+ });
1500
});
1501
1502
it('does not call lifecycles of a suspended component', async () => {