Skip to content

Commit 059223d

Browse files
committed
Denote suspenseful components with comment markers
1 parent ae6450b commit 059223d

File tree

2 files changed

+148
-37
lines changed

2 files changed

+148
-37
lines changed

src/index.js

+45-18
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export function renderToString(vnode, context, _rendererState) {
6565
_rendererState
6666
);
6767

68-
if (Array.isArray(rendered)) {
68+
if (isArray(rendered)) {
6969
return rendered.join('');
7070
}
7171
return rendered;
@@ -119,7 +119,7 @@ export async function renderToStringAsync(vnode, context) {
119119
undefined
120120
);
121121

122-
if (Array.isArray(rendered)) {
122+
if (isArray(rendered)) {
123123
let count = 0;
124124
let resolved = rendered;
125125

@@ -150,6 +150,8 @@ function markAsDirty() {
150150
}
151151

152152
const EMPTY_OBJ = {};
153+
const BEGIN_SUSPENSE_DENOMINATOR = '<!-- $s -->';
154+
const END_SUSPENSE_DENOMINATOR = '<!-- /$s -->';
153155

154156
/**
155157
* @param {VNode} vnode
@@ -368,7 +370,12 @@ function _renderToString(
368370

369371
if (renderHook) renderHook(vnode);
370372

371-
rendered = type.call(component, props, cctx);
373+
try {
374+
rendered = type.call(component, props, cctx);
375+
} catch (e) {
376+
if (asyncMode) vnode._suspended = true;
377+
throw e;
378+
}
372379
}
373380
component[DIRTY] = true;
374381
}
@@ -398,6 +405,7 @@ function _renderToString(
398405
selectValue,
399406
vnode,
400407
asyncMode,
408+
false,
401409
renderer
402410
);
403411
return str;
@@ -472,6 +480,21 @@ function _renderToString(
472480

473481
if (options.unmount) options.unmount(vnode);
474482

483+
if (vnode._suspended) {
484+
if (typeof str === 'string') {
485+
return BEGIN_SUSPENSE_DENOMINATOR + str + END_SUSPENSE_DENOMINATOR;
486+
} else if (isArray(str)) {
487+
str.unshift(BEGIN_SUSPENSE_DENOMINATOR);
488+
str.push(END_SUSPENSE_DENOMINATOR);
489+
return str;
490+
}
491+
492+
return str.then(
493+
(resolved) =>
494+
BEGIN_SUSPENSE_DENOMINATOR + resolved + END_SUSPENSE_DENOMINATOR
495+
);
496+
}
497+
475498
return str;
476499
} catch (error) {
477500
if (!asyncMode && renderer && renderer.onError) {
@@ -500,7 +523,7 @@ function _renderToString(
500523

501524
const renderNestedChildren = () => {
502525
try {
503-
return _renderToString(
526+
const result = _renderToString(
504527
rendered,
505528
context,
506529
isSvgMode,
@@ -509,26 +532,30 @@ function _renderToString(
509532
asyncMode,
510533
renderer
511534
);
535+
return vnode._suspended
536+
? BEGIN_SUSPENSE_DENOMINATOR + result + END_SUSPENSE_DENOMINATOR
537+
: result;
512538
} catch (e) {
513539
if (!e || typeof e.then !== 'function') throw e;
514540

515-
return e.then(
516-
() =>
517-
_renderToString(
518-
rendered,
519-
context,
520-
isSvgMode,
521-
selectValue,
522-
vnode,
523-
asyncMode,
524-
renderer
525-
),
526-
() => renderNestedChildren()
527-
);
541+
return e.then(() => {
542+
const result = _renderToString(
543+
rendered,
544+
context,
545+
isSvgMode,
546+
selectValue,
547+
vnode,
548+
asyncMode,
549+
renderer
550+
);
551+
return vnode._suspended
552+
? BEGIN_SUSPENSE_DENOMINATOR + result + END_SUSPENSE_DENOMINATOR
553+
: result;
554+
}, renderNestedChildren);
528555
}
529556
};
530557

531-
return error.then(() => renderNestedChildren());
558+
return error.then(renderNestedChildren);
532559
}
533560
}
534561

test/compat/async.test.jsx

+103-19
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,30 @@ describe('Async renderToString', () => {
1616
</Suspense>
1717
);
1818

19-
const expected = `<div class="foo">bar</div>`;
19+
const expected = `<!-- $s --><div class="foo">bar</div><!-- /$s -->`;
20+
21+
suspended.resolve();
22+
23+
const rendered = await promise;
24+
25+
expect(rendered).to.equal(expected);
26+
});
27+
28+
it('should correctly denote null returns of suspending components', async () => {
29+
const { Suspender, suspended } = createSuspender();
30+
31+
const Analytics = () => null;
32+
33+
const promise = renderToStringAsync(
34+
<Suspense fallback={<div>loading...</div>}>
35+
<Suspender>
36+
<Analytics />
37+
</Suspender>
38+
<div class="foo">bar</div>
39+
</Suspense>
40+
);
41+
42+
const expected = `<!-- $s --><!-- /$s --><div class="foo">bar</div>`;
2043

2144
suspended.resolve();
2245

@@ -26,10 +49,14 @@ describe('Async renderToString', () => {
2649
});
2750

2851
it('should render JSX with nested suspended components', async () => {
29-
const { Suspender: SuspenderOne, suspended: suspendedOne } =
30-
createSuspender();
31-
const { Suspender: SuspenderTwo, suspended: suspendedTwo } =
32-
createSuspender();
52+
const {
53+
Suspender: SuspenderOne,
54+
suspended: suspendedOne
55+
} = createSuspender();
56+
const {
57+
Suspender: SuspenderTwo,
58+
suspended: suspendedTwo
59+
} = createSuspender();
3360

3461
const promise = renderToStringAsync(
3562
<ul>
@@ -45,7 +72,7 @@ describe('Async renderToString', () => {
4572
</ul>
4673
);
4774

48-
const expected = `<ul><li>one</li><li>two</li><li>three</li></ul>`;
75+
const expected = `<ul><!-- $s --><li>one</li><!-- $s --><li>two</li><!-- /$s --><li>three</li><!-- /$s --></ul>`;
4976

5077
suspendedOne.resolve();
5178
suspendedTwo.resolve();
@@ -56,10 +83,14 @@ describe('Async renderToString', () => {
5683
});
5784

5885
it('should render JSX with nested suspense boundaries', async () => {
59-
const { Suspender: SuspenderOne, suspended: suspendedOne } =
60-
createSuspender();
61-
const { Suspender: SuspenderTwo, suspended: suspendedTwo } =
62-
createSuspender();
86+
const {
87+
Suspender: SuspenderOne,
88+
suspended: suspendedOne
89+
} = createSuspender();
90+
const {
91+
Suspender: SuspenderTwo,
92+
suspended: suspendedTwo
93+
} = createSuspender();
6394

6495
const promise = renderToStringAsync(
6596
<ul>
@@ -77,23 +108,76 @@ describe('Async renderToString', () => {
77108
</ul>
78109
);
79110

80-
const expected = `<ul><li>one</li><li>two</li><li>three</li></ul>`;
111+
const expected = `<ul><!-- $s --><li>one</li><!-- $s --><li>two</li><!-- /$s --><li>three</li><!-- /$s --></ul>`;
112+
113+
suspendedOne.resolve();
114+
suspendedTwo.resolve();
115+
116+
const rendered = await promise;
117+
118+
expect(rendered).to.equal(expected);
119+
});
120+
121+
// TODO
122+
it.skip('should render JSX with deeply nested suspense boundaries', async () => {
123+
const {
124+
Suspender: SuspenderOne,
125+
suspended: suspendedOne
126+
} = createSuspender();
127+
const {
128+
Suspender: SuspenderTwo,
129+
suspended: suspendedTwo
130+
} = createSuspender();
131+
const {
132+
Suspender: SuspenderThree,
133+
suspended: suspendedThree
134+
} = createSuspender();
135+
136+
const promise = renderToStringAsync(
137+
<ul>
138+
<Suspense fallback={null}>
139+
<SuspenderOne>
140+
<li>one</li>
141+
<Suspense fallback={null}>
142+
<SuspenderTwo>
143+
<li>two</li>
144+
<Suspense fallback={null}>
145+
<SuspenderThree>
146+
<li>three</li>
147+
</SuspenderThree>
148+
</Suspense>
149+
</SuspenderTwo>
150+
</Suspense>
151+
<li>four</li>
152+
</SuspenderOne>
153+
</Suspense>
154+
</ul>
155+
);
156+
157+
const expected = `<ul><!-- $s --><li>one</li><!-- $s --><li>two</li><!-- $s --><li>three</li><!-- /$s --><!-- /$s --><li>four</li><!-- /$s --></ul>`;
81158

82159
suspendedOne.resolve();
83160
suspendedTwo.resolve();
161+
suspendedThree.resolve();
84162

85163
const rendered = await promise;
86164

87165
expect(rendered).to.equal(expected);
88166
});
89167

90168
it('should render JSX with multiple suspended direct children within a single suspense boundary', async () => {
91-
const { Suspender: SuspenderOne, suspended: suspendedOne } =
92-
createSuspender();
93-
const { Suspender: SuspenderTwo, suspended: suspendedTwo } =
94-
createSuspender();
95-
const { Suspender: SuspenderThree, suspended: suspendedThree } =
96-
createSuspender();
169+
const {
170+
Suspender: SuspenderOne,
171+
suspended: suspendedOne
172+
} = createSuspender();
173+
const {
174+
Suspender: SuspenderTwo,
175+
suspended: suspendedTwo
176+
} = createSuspender();
177+
const {
178+
Suspender: SuspenderThree,
179+
suspended: suspendedThree
180+
} = createSuspender();
97181

98182
const promise = renderToStringAsync(
99183
<ul>
@@ -113,7 +197,7 @@ describe('Async renderToString', () => {
113197
</ul>
114198
);
115199

116-
const expected = `<ul><li>one</li><li>two</li><li>three</li></ul>`;
200+
const expected = `<ul><!-- $s --><li>one</li><!-- /$s --><!-- $s --><li>two</li><!-- /$s --><!-- $s --><li>three</li><!-- /$s --></ul>`;
117201

118202
suspendedOne.resolve();
119203
suspendedTwo.resolve();
@@ -173,6 +257,6 @@ describe('Async renderToString', () => {
173257

174258
suspended.resolve();
175259
const rendered = await promise;
176-
expect(rendered).to.equal('<p>ok</p>');
260+
expect(rendered).to.equal('<!-- $s --><p>ok</p><!-- /$s -->');
177261
});
178262
});

0 commit comments

Comments
 (0)