@samitouri / QOS-React / commits / 02e4848e3a

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.');