Improved suspense support in ReactDOMServer (#14161)
Alex Taylor committed
Nov 8, 2018 at 18:15 UTC
02e4848e3aeb072214690d655c9cfe717c290491
2 files changed
+103
-21
packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js
+38
-6
@@ -43,16 +43,48 @@ describe('ReactDOMServerSuspense', () => {
43
resetModules();
44
});
45
46
- it('should always render the fallback when a placeholder is encountered', async () => {
47
- const Suspended = props => {
48
- throw new Promise(() => {});
49
- };
46
+ function Text(props) {
47
+ return <div>{props.text}</div>;
48
+ }
49
+
50
+ function AsyncText(props) {
51
+ throw new Promise(() => {});
52
+ }
53
+
54
+ it('should render the children when no promise is thrown', async () => {
55
const e = await serverRender(
51
- <React.Suspense fallback={<div />}>
52
- <Suspended />
56
+ <React.Suspense fallback={<Text text="Fallback" />}>
57
+ <Text text="Children" />
58
</React.Suspense>,
59
);
60
61
expect(e.tagName).toBe('DIV');
62
+ expect(e.textContent).toBe('Children');
63
+ });
64
+
65
+ it('should render the fallback when a promise thrown', async () => {
66
+ const e = await serverRender(
67
+ <React.Suspense fallback={<Text text="Fallback" />}>
68
+ <AsyncText text="Children" />
69
+ </React.Suspense>,
70
+ );
71
+
72
+ expect(e.tagName).toBe('DIV');
73
+ expect(e.textContent).toBe('Fallback');
74
+ });
75
+
76
+ it('should work with nested suspense components', async () => {
77
+ const e = await serverRender(
78
+ <React.Suspense fallback={<Text text="Fallback" />}>
79
+ <div>
80
+ <Text text="Children" />
81
+ <React.Suspense fallback={<Text text="Fallback" />}>
82
+ <AsyncText text="Children" />
83
+ </React.Suspense>
84
+ </div>
85
+ </React.Suspense>,
86
+ );
87
+
88
+ expect(e.innerHTML).toBe('<div>Children</div><div>Fallback</div>');
89
});
90
});
packages/react-dom/src/server/ReactPartialRenderer.js
+65
-15
@@ -707,6 +707,7 @@ type Frame = {
707
type: mixed,
708
domNamespace: string,
709
children: FlatReactChildren,
710
+ fallbackFrame?: Frame,
711
childIndex: number,
712
context: Object,
713
footer: string,
@@ -723,6 +724,7 @@ class ReactDOMServerRenderer {
724
currentSelectValue: any;
725
previousWasTextNode: boolean;
726
makeStaticMarkup: boolean;
727
+ suspenseDepth: number;
728
729
contextIndex: number;
730
contextStack: Array<ReactContext<any>>;
@@ -750,6 +752,7 @@ class ReactDOMServerRenderer {
752
this.currentSelectValue = null;
753
this.previousWasTextNode = false;
754
this.makeStaticMarkup = makeStaticMarkup;
755
+ this.suspenseDepth = 0;
756
757
// Context (new API)
758
this.contextIndex = -1;
@@ -825,16 +828,18 @@ class ReactDOMServerRenderer {
828
ReactCurrentOwner.currentDispatcher = DispatcherWithoutHooks;
829
}
830
try {
828
- let out = '';
829
- while (out.length < bytes) {
831
+ // Markup generated within <Suspense> ends up buffered until we know
832
+ // nothing in that boundary suspended
833
+ let out = [''];
834
+ let suspended = false;
835
+ while (out[0].length < bytes) {
836
if (this.stack.length === 0) {
837
this.exhausted = true;
838
break;
839
}
840
const frame: Frame = this.stack[this.stack.length - 1];
835
- if (frame.childIndex >= frame.children.length) {
841
+ if (suspended || frame.childIndex >= frame.children.length) {
842
const footer = frame.footer;
837
- out += footer;
843
if (footer !== '') {
844
this.previousWasTextNode = false;
845
}
@@ -848,26 +853,57 @@ class ReactDOMServerRenderer {
853
) {
854
const provider: ReactProvider<any> = (frame.type: any);
855
this.popProvider(provider);
856
+ } else if (frame.type === REACT_SUSPENSE_TYPE) {
857
+ this.suspenseDepth--;
858
+ const buffered = out.pop();
859
+
860
+ if (suspended) {
861
+ suspended = false;
862
+ // If rendering was suspended at this boundary, render the fallbackFrame
863
+ const fallbackFrame = frame.fallbackFrame;
864
+ invariant(
865
+ fallbackFrame,
866
+ 'suspense fallback not found, something is broken',
867
+ );
868
+ this.stack.push(fallbackFrame);
869
+ // Skip flushing output since we're switching to the fallback
870
+ continue;
871
+ } else {
872
+ out[this.suspenseDepth] += buffered;
873
+ }
874
}
875
+
876
+ // Flush output
877
+ out[this.suspenseDepth] += footer;
878
continue;
879
}
880
const child = frame.children[frame.childIndex++];
881
+
882
+ let outBuffer = '';
883
if (__DEV__) {
884
pushCurrentDebugStack(this.stack);
885
// We're starting work on this frame, so reset its inner stack.
886
((frame: any): FrameDev).debugElementStack.length = 0;
859
- try {
860
- // Be careful! Make sure this matches the PROD path below.
861
- out += this.render(child, frame.context, frame.domNamespace);
862
- } finally {
887
+ }
888
+ try {
889
+ outBuffer += this.render(child, frame.context, frame.domNamespace);
890
+ } catch (err) {
891
+ if (enableSuspenseServerRenderer && typeof err.then === 'function') {
892
+ suspended = true;
893
+ } else {
894
+ throw err;
895
+ }
896
+ } finally {
897
+ if (__DEV__) {
898
popCurrentDebugStack();
899
}
865
- } else {
866
- // Be careful! Make sure this matches the DEV path above.
867
- out += this.render(child, frame.context, frame.domNamespace);
900
}
901
+ if (out.length <= this.suspenseDepth) {
902
+ out.push('');
903
+ }
904
+ out[this.suspenseDepth] += outBuffer;
905
}
870
- return out;
906
+ return out[0];
907
} finally {
908
ReactCurrentOwner.currentDispatcher = prevDispatcher;
909
}
@@ -960,13 +996,25 @@ class ReactDOMServerRenderer {
996
}
997
case REACT_SUSPENSE_TYPE: {
998
if (enableSuspenseServerRenderer) {
963
- const nextChildren = toArray(
964
- // Always use the fallback when synchronously rendering to string.
999
+ const fallbackChildren = toArray(
1000
((nextChild: any): ReactElement).props.fallback,
1001
);
967
- const frame: Frame = {
1002
+ const nextChildren = toArray(
1003
+ ((nextChild: any): ReactElement).props.children,
1004
+ );
1005
+ const fallbackFrame: Frame = {
1006
type: null,
1007
domNamespace: parentNamespace,
1008
+ children: fallbackChildren,
1009
+ childIndex: 0,
1010
+ context: context,
1011
+ footer: '',
1012
+ out: '',
1013
+ };
1014
+ const frame: Frame = {
1015
+ fallbackFrame,
1016
+ type: REACT_SUSPENSE_TYPE,
1017
+ domNamespace: parentNamespace,
1018
children: nextChildren,
1019
childIndex: 0,
1020
context: context,
@@ -974,8 +1022,10 @@ class ReactDOMServerRenderer {
1022
};
1023
if (__DEV__) {
1024
((frame: any): FrameDev).debugElementStack = [];
1025
+ ((fallbackFrame: any): FrameDev).debugElementStack = [];
1026
}
1027
this.stack.push(frame);
1028
+ this.suspenseDepth++;
1029
return '';
1030
} else {
1031
invariant(false, 'ReactDOMServer does not yet support Suspense.');