Skip to content

Commit 02e4848

Browse files
authored
Improved suspense support in ReactDOMServer (facebook#14161)
1 parent 4b163fe commit 02e4848

File tree

2 files changed

+103
-21
lines changed

2 files changed

+103
-21
lines changed

packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js

+38-6
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,48 @@ describe('ReactDOMServerSuspense', () => {
4343
resetModules();
4444
});
4545

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 () => {
5055
const e = await serverRender(
51-
<React.Suspense fallback={<div />}>
52-
<Suspended />
56+
<React.Suspense fallback={<Text text="Fallback" />}>
57+
<Text text="Children" />
5358
</React.Suspense>,
5459
);
5560

5661
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>');
5789
});
5890
});

packages/react-dom/src/server/ReactPartialRenderer.js

+65-15
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,7 @@ type Frame = {
707707
type: mixed,
708708
domNamespace: string,
709709
children: FlatReactChildren,
710+
fallbackFrame?: Frame,
710711
childIndex: number,
711712
context: Object,
712713
footer: string,
@@ -723,6 +724,7 @@ class ReactDOMServerRenderer {
723724
currentSelectValue: any;
724725
previousWasTextNode: boolean;
725726
makeStaticMarkup: boolean;
727+
suspenseDepth: number;
726728

727729
contextIndex: number;
728730
contextStack: Array<ReactContext<any>>;
@@ -750,6 +752,7 @@ class ReactDOMServerRenderer {
750752
this.currentSelectValue = null;
751753
this.previousWasTextNode = false;
752754
this.makeStaticMarkup = makeStaticMarkup;
755+
this.suspenseDepth = 0;
753756

754757
// Context (new API)
755758
this.contextIndex = -1;
@@ -825,16 +828,18 @@ class ReactDOMServerRenderer {
825828
ReactCurrentOwner.currentDispatcher = DispatcherWithoutHooks;
826829
}
827830
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) {
830836
if (this.stack.length === 0) {
831837
this.exhausted = true;
832838
break;
833839
}
834840
const frame: Frame = this.stack[this.stack.length - 1];
835-
if (frame.childIndex >= frame.children.length) {
841+
if (suspended || frame.childIndex >= frame.children.length) {
836842
const footer = frame.footer;
837-
out += footer;
838843
if (footer !== '') {
839844
this.previousWasTextNode = false;
840845
}
@@ -848,26 +853,57 @@ class ReactDOMServerRenderer {
848853
) {
849854
const provider: ReactProvider<any> = (frame.type: any);
850855
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+
}
851874
}
875+
876+
// Flush output
877+
out[this.suspenseDepth] += footer;
852878
continue;
853879
}
854880
const child = frame.children[frame.childIndex++];
881+
882+
let outBuffer = '';
855883
if (__DEV__) {
856884
pushCurrentDebugStack(this.stack);
857885
// We're starting work on this frame, so reset its inner stack.
858886
((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__) {
863898
popCurrentDebugStack();
864899
}
865-
} else {
866-
// Be careful! Make sure this matches the DEV path above.
867-
out += this.render(child, frame.context, frame.domNamespace);
868900
}
901+
if (out.length <= this.suspenseDepth) {
902+
out.push('');
903+
}
904+
out[this.suspenseDepth] += outBuffer;
869905
}
870-
return out;
906+
return out[0];
871907
} finally {
872908
ReactCurrentOwner.currentDispatcher = prevDispatcher;
873909
}
@@ -960,22 +996,36 @@ class ReactDOMServerRenderer {
960996
}
961997
case REACT_SUSPENSE_TYPE: {
962998
if (enableSuspenseServerRenderer) {
963-
const nextChildren = toArray(
964-
// Always use the fallback when synchronously rendering to string.
999+
const fallbackChildren = toArray(
9651000
((nextChild: any): ReactElement).props.fallback,
9661001
);
967-
const frame: Frame = {
1002+
const nextChildren = toArray(
1003+
((nextChild: any): ReactElement).props.children,
1004+
);
1005+
const fallbackFrame: Frame = {
9681006
type: null,
9691007
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,
9701018
children: nextChildren,
9711019
childIndex: 0,
9721020
context: context,
9731021
footer: '',
9741022
};
9751023
if (__DEV__) {
9761024
((frame: any): FrameDev).debugElementStack = [];
1025+
((fallbackFrame: any): FrameDev).debugElementStack = [];
9771026
}
9781027
this.stack.push(frame);
1028+
this.suspenseDepth++;
9791029
return '';
9801030
} else {
9811031
invariant(false, 'ReactDOMServer does not yet support Suspense.');

0 commit comments

Comments
 (0)