@samitouri / QOS-React / commits / 9bcd5b2576

Fix Suspense fragment edge cases

Dan committed Apr 5, 2019 at 00:29 UTC 9bcd5b25761f63755ca98575ee0107e1defbeb97
3 files changed +94 -2
shells/dev/app/SuspenseTree/index.js new
+64
@@ -0,0 +1,64 @@
1 +// @flow
2 +
3 +import React, { Suspense, useState } from 'react';
4 +
5 +function SuspenseTree() {
6 + return (
7 + <>
8 + <h1>Suspense</h1>
9 + <Suspense fallback={<Fallback>Loading outer</Fallback>}>
10 + <Parent />
11 + </Suspense>
12 + </>
13 + );
14 +}
15 +
16 +function Parent() {
17 + return (
18 + <div>
19 + <Suspense fallback={<Fallback>Loading inner 1</Fallback>}>
20 + <Child>Hello</Child>
21 + </Suspense>
22 + <Suspense fallback={<Fallback>Loading inner 2</Fallback>}>
23 + <Child>World</Child>
24 + </Suspense>
25 + <Suspense fallback={<Fallback>This will never load</Fallback>}>
26 + <Never />
27 + </Suspense>
28 + <LoadLater />
29 + </div>
30 + );
31 +}
32 +
33 +function LoadLater() {
34 + const [loadChild, setLoadChild] = useState(0);
35 + return (
36 + <Suspense
37 + fallback={
38 + <Fallback onClick={() => setLoadChild(true)}>Click to load</Fallback>
39 + }
40 + >
41 + {loadChild ? (
42 + <Child onClick={() => setLoadChild(false)}>
43 + Loaded! Click to suspend again.
44 + </Child>
45 + ) : (
46 + <Never />
47 + )}
48 + </Suspense>
49 + );
50 +}
51 +
52 +function Child(props) {
53 + return <p {...props} />;
54 +}
55 +
56 +function Fallback(props) {
57 + return <h3 {...props}>{props.children}</h3>;
58 +}
59 +
60 +function Never() {
61 + throw new Promise(resolve => {});
62 +}
63 +
64 +export default SuspenseTree;
shells/dev/app/index.js
+2
@@ -11,6 +11,7 @@ import InspectableElements from './InspectableElements';
11 import InteractionTracing from './InteractionTracing';
12 import ToDoList from './ToDoList';
13 import Toggle from './Toggle';
14 +import SuspenseTree from './SuspenseTree';
15
16 import './styles.css';
17
@@ -33,6 +34,7 @@ function mountTestApp() {
34 mountHelper(ElementTypes);
35 mountHelper(EditableProps);
36 mountHelper(Toggle);
37 + mountHelper(SuspenseTree);
38 mountHelper(DeeplyNestedComponents);
39 }
40
src/backend/renderer.js
+28 -2
@@ -716,6 +716,16 @@ export function attach(
716 function enqueueUnmount(fiber) {
717 const isRoot = fiber.tag === HostRoot;
718 const primaryFiber = getPrimaryFiber(fiber);
719 + if (!fiberToIDMap.has(primaryFiber)) {
720 + // If we've never seen this Fiber, it might be because
721 + // it is inside a non-current Suspense fragment tree,
722 + // and so the store is not even aware of it.
723 + // In that case we can just ignore it, or otherwise
724 + // there will be errors later on.
725 + primaryFibers.delete(primaryFiber);
726 + // TODO: this is fragile and can obscure actual bugs.
727 + return;
728 + }
729 const id = getFiberID(primaryFiber);
730 if (isRoot) {
731 const operation = new Uint32Array(2);
@@ -757,8 +767,24 @@ export function attach(
767 enqueueMount(fiber, parentFiber);
768 }
769
760 - if (fiber.child !== null) {
761 - mountFiber(fiber.child, shouldEnqueueMount ? fiber : parentFiber, true);
770 + const isTimedOutSuspense =
771 + fiber.tag === ReactTypeOfWork.SuspenseComponent &&
772 + fiber.memoizedState !== null;
773 +
774 + if (isTimedOutSuspense) {
775 + // Special case: if Suspense mounts in a timed-out state,
776 + // get the fallback child from the inner fragment and mount
777 + // it as if it was our own child. Updates handle this too.
778 + const primaryChildFragment = fiber.child;
779 + const fallbackChildFragment = primaryChildFragment.sibling;
780 + const fallbackChild = fallbackChildFragment.child;
781 + if (fallbackChild !== null) {
782 + mountFiber(fallbackChild, shouldEnqueueMount ? fiber : parentFiber, true);
783 + }
784 + } else {
785 + if (fiber.child !== null) {
786 + mountFiber(fiber.child, shouldEnqueueMount ? fiber : parentFiber, true);
787 + }
788 }
789
790 if (traverseSiblings && fiber.sibling !== null) {