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