Skip to content

Commit 7f7b1e7

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

File tree

2 files changed

+195
-37
lines changed

2 files changed

+195
-37
lines changed

src/index.js

+48-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,14 @@ function _renderToString(
368370

369371
if (renderHook) renderHook(vnode);
370372

371-
rendered = type.call(component, props, cctx);
373+
try {
374+
console.log('rendering', vnode.type, vnode.props);
375+
rendered = type.call(component, props, cctx);
376+
} catch (e) {
377+
console.log('caught', vnode.type, vnode.props);
378+
if (asyncMode) vnode._suspended = true;
379+
throw e;
380+
}
372381
}
373382
component[DIRTY] = true;
374383
}
@@ -398,6 +407,7 @@ function _renderToString(
398407
selectValue,
399408
vnode,
400409
asyncMode,
410+
false,
401411
renderer
402412
);
403413
return str;
@@ -472,6 +482,22 @@ function _renderToString(
472482

473483
if (options.unmount) options.unmount(vnode);
474484

485+
if (vnode._suspended) {
486+
console.log('suspended', vnode.type, str);
487+
if (typeof str === 'string') {
488+
return BEGIN_SUSPENSE_DENOMINATOR + str + END_SUSPENSE_DENOMINATOR;
489+
} else if (isArray(str)) {
490+
str.unshift(BEGIN_SUSPENSE_DENOMINATOR);
491+
str.push(END_SUSPENSE_DENOMINATOR);
492+
return str;
493+
}
494+
495+
return str.then(
496+
(resolved) =>
497+
BEGIN_SUSPENSE_DENOMINATOR + resolved + END_SUSPENSE_DENOMINATOR
498+
);
499+
}
500+
475501
return str;
476502
} catch (error) {
477503
if (!asyncMode && renderer && renderer.onError) {
@@ -500,7 +526,7 @@ function _renderToString(
500526

501527
const renderNestedChildren = () => {
502528
try {
503-
return _renderToString(
529+
const result = _renderToString(
504530
rendered,
505531
context,
506532
isSvgMode,
@@ -509,26 +535,30 @@ function _renderToString(
509535
asyncMode,
510536
renderer
511537
);
538+
return vnode._suspended
539+
? BEGIN_SUSPENSE_DENOMINATOR + result + END_SUSPENSE_DENOMINATOR
540+
: result;
512541
} catch (e) {
513542
if (!e || typeof e.then !== 'function') throw e;
514543

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-
);
544+
return e.then(() => {
545+
const result = _renderToString(
546+
rendered,
547+
context,
548+
isSvgMode,
549+
selectValue,
550+
vnode,
551+
asyncMode,
552+
renderer
553+
);
554+
return vnode._suspended
555+
? BEGIN_SUSPENSE_DENOMINATOR + result + END_SUSPENSE_DENOMINATOR
556+
: result;
557+
}, renderNestedChildren);
528558
}
529559
};
530560

531-
return error.then(() => renderNestedChildren());
561+
return error.then(renderNestedChildren);
532562
}
533563
}
534564

test/compat/async.test.jsx

+147-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,120 @@ 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+
it.skip('should render JSX with nested suspense boundaries containing multiple suspending components', async () => {
122+
const {
123+
Suspender: SuspenderOne,
124+
suspended: suspendedOne
125+
} = createSuspender();
126+
const {
127+
Suspender: SuspenderTwo,
128+
suspended: suspendedTwo
129+
} = createSuspender();
130+
const {
131+
Suspender: SuspenderThree,
132+
suspended: suspendedThree
133+
} = createSuspender();
134+
135+
const promise = renderToStringAsync(
136+
<ul>
137+
<Suspense fallback={null}>
138+
<SuspenderOne>
139+
<li>one</li>
140+
<Suspense fallback={null}>
141+
<SuspenderTwo>
142+
<li>two</li>
143+
</SuspenderTwo>
144+
<SuspenderThree>
145+
<li>three</li>
146+
</SuspenderThree>
147+
</Suspense>
148+
<li>four</li>
149+
</SuspenderOne>
150+
</Suspense>
151+
</ul>
152+
);
153+
154+
const expected = `<ul><!-- $s --><li>one</li><!-- $s --><li>two</li><!-- /$s --><!-- $s --><li>three</li><!-- /$s --><li>four</li><!-- /$s --></ul>`;
81155

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

85207
const rendered = await promise;
86208

87209
expect(rendered).to.equal(expected);
88210
});
89211

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

98226
const promise = renderToStringAsync(
99227
<ul>
@@ -113,7 +241,7 @@ describe('Async renderToString', () => {
113241
</ul>
114242
);
115243

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

118246
suspendedOne.resolve();
119247
suspendedTwo.resolve();
@@ -173,6 +301,6 @@ describe('Async renderToString', () => {
173301

174302
suspended.resolve();
175303
const rendered = await promise;
176-
expect(rendered).to.equal('<p>ok</p>');
304+
expect(rendered).to.equal('<!-- $s --><p>ok</p><!-- /$s -->');
177305
});
178306
});

0 commit comments

Comments
 (0)