Skip to content

Commit b0994fa

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

File tree

2 files changed

+194
-37
lines changed

2 files changed

+194
-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

+149-19
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { h } from 'preact';
33
import { Suspense, useId } from 'preact/compat';
44
import { expect } from 'chai';
55
import { createSuspender } from '../utils.jsx';
6+
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
67

78
describe('Async renderToString', () => {
89
it('should render JSX after a suspense boundary', async () => {
@@ -16,7 +17,30 @@ describe('Async renderToString', () => {
1617
</Suspense>
1718
);
1819

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

2145
suspended.resolve();
2246

@@ -26,10 +50,14 @@ describe('Async renderToString', () => {
2650
});
2751

2852
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();
53+
const {
54+
Suspender: SuspenderOne,
55+
suspended: suspendedOne
56+
} = createSuspender();
57+
const {
58+
Suspender: SuspenderTwo,
59+
suspended: suspendedTwo
60+
} = createSuspender();
3361

3462
const promise = renderToStringAsync(
3563
<ul>
@@ -45,7 +73,7 @@ describe('Async renderToString', () => {
4573
</ul>
4674
);
4775

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

5078
suspendedOne.resolve();
5179
suspendedTwo.resolve();
@@ -56,10 +84,14 @@ describe('Async renderToString', () => {
5684
});
5785

5886
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();
87+
const {
88+
Suspender: SuspenderOne,
89+
suspended: suspendedOne
90+
} = createSuspender();
91+
const {
92+
Suspender: SuspenderTwo,
93+
suspended: suspendedTwo
94+
} = createSuspender();
6395

6496
const promise = renderToStringAsync(
6597
<ul>
@@ -77,23 +109,121 @@ describe('Async renderToString', () => {
77109
</ul>
78110
);
79111

80-
const expected = `<ul><li>one</li><li>two</li><li>three</li></ul>`;
112+
const expected = `<ul><!-- $s --><li>one</li><!-- $s --><li>two</li><!-- /$s --><li>three</li><!-- /$s --></ul>`;
113+
114+
suspendedOne.resolve();
115+
suspendedTwo.resolve();
116+
117+
const rendered = await promise;
118+
119+
expect(rendered).to.equal(expected);
120+
});
121+
122+
it('should render JSX with nested suspense boundaries containing multiple suspending components', 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('three');
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+
</SuspenderTwo>
145+
<SuspenderThree>
146+
<li>three</li>
147+
</SuspenderThree>
148+
</Suspense>
149+
<li>four</li>
150+
</SuspenderOne>
151+
</Suspense>
152+
</ul>
153+
);
154+
155+
const expected = `<ul><!-- $s --><li>one</li><!-- $s --><li>two</li><!-- /$s --><!-- $s --><li>three</li><!-- /$s --><li>four</li><!-- /$s --></ul>`;
81156

82157
suspendedOne.resolve();
83158
suspendedTwo.resolve();
159+
await wait(0);
160+
suspendedThree.resolve();
161+
162+
const rendered = await promise;
163+
164+
expect(rendered).to.equal(expected);
165+
});
166+
167+
it.only('should render JSX with deeply nested suspense boundaries', async () => {
168+
const {
169+
Suspender: SuspenderOne,
170+
suspended: suspendedOne
171+
} = createSuspender();
172+
const {
173+
Suspender: SuspenderTwo,
174+
suspended: suspendedTwo
175+
} = createSuspender();
176+
const {
177+
Suspender: SuspenderThree,
178+
suspended: suspendedThree
179+
} = createSuspender();
180+
181+
const promise = renderToStringAsync(
182+
<ul>
183+
<Suspense fallback={null}>
184+
<SuspenderOne>
185+
<li>one</li>
186+
<Suspense fallback={null}>
187+
<SuspenderTwo>
188+
<li>two</li>
189+
<Suspense fallback={null}>
190+
<SuspenderThree>
191+
<li>three</li>
192+
</SuspenderThree>
193+
</Suspense>
194+
</SuspenderTwo>
195+
</Suspense>
196+
<li>four</li>
197+
</SuspenderOne>
198+
</Suspense>
199+
</ul>
200+
);
201+
202+
const expected = `<ul><!-- $s --><li>one</li><!-- $s --><li>two</li><!-- $s --><li>three</li><!-- /$s --><!-- /$s --><li>four</li><!-- /$s --></ul>`;
203+
204+
suspendedOne.resolve();
205+
suspendedTwo.resolve();
206+
await wait(0);
207+
suspendedThree.resolve();
84208

85209
const rendered = await promise;
86210

87211
expect(rendered).to.equal(expected);
88212
});
89213

90214
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();
215+
const {
216+
Suspender: SuspenderOne,
217+
suspended: suspendedOne
218+
} = createSuspender();
219+
const {
220+
Suspender: SuspenderTwo,
221+
suspended: suspendedTwo
222+
} = createSuspender();
223+
const {
224+
Suspender: SuspenderThree,
225+
suspended: suspendedThree
226+
} = createSuspender();
97227

98228
const promise = renderToStringAsync(
99229
<ul>
@@ -113,7 +243,7 @@ describe('Async renderToString', () => {
113243
</ul>
114244
);
115245

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

118248
suspendedOne.resolve();
119249
suspendedTwo.resolve();
@@ -173,6 +303,6 @@ describe('Async renderToString', () => {
173303

174304
suspended.resolve();
175305
const rendered = await promise;
176-
expect(rendered).to.equal('<p>ok</p>');
306+
expect(rendered).to.equal('<!-- $s --><p>ok</p><!-- /$s -->');
177307
});
178308
});

0 commit comments

Comments
 (0)