@samitouri / QOS-React / commits / 235a6c4af6

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 () => {