Skip to content

Commit

Permalink
Denote suspenseful components with comment markers
Browse files Browse the repository at this point in the history
  • Loading branch information
JoviDeCroock committed Jul 14, 2024
1 parent ae6450b commit b0994fa
Show file tree
Hide file tree
Showing 2 changed files with 194 additions and 37 deletions.
63 changes: 45 additions & 18 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function renderToString(vnode, context, _rendererState) {
_rendererState
);

if (Array.isArray(rendered)) {
if (isArray(rendered)) {
return rendered.join('');
}
return rendered;
Expand Down Expand Up @@ -119,7 +119,7 @@ export async function renderToStringAsync(vnode, context) {
undefined
);

if (Array.isArray(rendered)) {
if (isArray(rendered)) {
let count = 0;
let resolved = rendered;

Expand Down Expand Up @@ -150,6 +150,8 @@ function markAsDirty() {
}

const EMPTY_OBJ = {};
const BEGIN_SUSPENSE_DENOMINATOR = '<!-- $s -->';
const END_SUSPENSE_DENOMINATOR = '<!-- /$s -->';

/**
* @param {VNode} vnode
Expand Down Expand Up @@ -368,7 +370,12 @@ function _renderToString(

if (renderHook) renderHook(vnode);

rendered = type.call(component, props, cctx);
try {
rendered = type.call(component, props, cctx);
} catch (e) {
if (asyncMode) vnode._suspended = true;
throw e;
}
}
component[DIRTY] = true;
}
Expand Down Expand Up @@ -398,6 +405,7 @@ function _renderToString(
selectValue,
vnode,
asyncMode,
false,
renderer
);
return str;
Expand Down Expand Up @@ -472,6 +480,21 @@ function _renderToString(

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

if (vnode._suspended) {
if (typeof str === 'string') {
return BEGIN_SUSPENSE_DENOMINATOR + str + END_SUSPENSE_DENOMINATOR;
} else if (isArray(str)) {
str.unshift(BEGIN_SUSPENSE_DENOMINATOR);
str.push(END_SUSPENSE_DENOMINATOR);
return str;
}

return str.then(
(resolved) =>
BEGIN_SUSPENSE_DENOMINATOR + resolved + END_SUSPENSE_DENOMINATOR
);
}

return str;
} catch (error) {
if (!asyncMode && renderer && renderer.onError) {
Expand Down Expand Up @@ -500,7 +523,7 @@ function _renderToString(

const renderNestedChildren = () => {
try {
return _renderToString(
const result = _renderToString(
rendered,
context,
isSvgMode,
Expand All @@ -509,26 +532,30 @@ function _renderToString(
asyncMode,
renderer
);
return vnode._suspended
? BEGIN_SUSPENSE_DENOMINATOR + result + END_SUSPENSE_DENOMINATOR
: result;
} catch (e) {
if (!e || typeof e.then !== 'function') throw e;

return e.then(
() =>
_renderToString(
rendered,
context,
isSvgMode,
selectValue,
vnode,
asyncMode,
renderer
),
() => renderNestedChildren()
);
return e.then(() => {
const result = _renderToString(
rendered,
context,
isSvgMode,
selectValue,
vnode,
asyncMode,
renderer
);
return vnode._suspended
? BEGIN_SUSPENSE_DENOMINATOR + result + END_SUSPENSE_DENOMINATOR
: result;
}, renderNestedChildren);
}
};

return error.then(() => renderNestedChildren());
return error.then(renderNestedChildren);
}
}

Expand Down
168 changes: 149 additions & 19 deletions test/compat/async.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { h } from 'preact';
import { Suspense, useId } from 'preact/compat';
import { expect } from 'chai';
import { createSuspender } from '../utils.jsx';
const wait = (ms) => new Promise((r) => setTimeout(r, ms));

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

const expected = `<div class="foo">bar</div>`;
const expected = `<!-- $s --><div class="foo">bar</div><!-- /$s -->`;

suspended.resolve();

const rendered = await promise;

expect(rendered).to.equal(expected);
});

it('should correctly denote null returns of suspending components', async () => {
const { Suspender, suspended } = createSuspender();

const Analytics = () => null;

const promise = renderToStringAsync(
<Suspense fallback={<div>loading...</div>}>
<Suspender>
<Analytics />
</Suspender>
<div class="foo">bar</div>
</Suspense>
);

const expected = `<!-- $s --><!-- /$s --><div class="foo">bar</div>`;

suspended.resolve();

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

it('should render JSX with nested suspended components', async () => {
const { Suspender: SuspenderOne, suspended: suspendedOne } =
createSuspender();
const { Suspender: SuspenderTwo, suspended: suspendedTwo } =
createSuspender();
const {
Suspender: SuspenderOne,
suspended: suspendedOne
} = createSuspender();
const {
Suspender: SuspenderTwo,
suspended: suspendedTwo
} = createSuspender();

const promise = renderToStringAsync(
<ul>
Expand All @@ -45,7 +73,7 @@ describe('Async renderToString', () => {
</ul>
);

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

suspendedOne.resolve();
suspendedTwo.resolve();
Expand All @@ -56,10 +84,14 @@ describe('Async renderToString', () => {
});

it('should render JSX with nested suspense boundaries', async () => {
const { Suspender: SuspenderOne, suspended: suspendedOne } =
createSuspender();
const { Suspender: SuspenderTwo, suspended: suspendedTwo } =
createSuspender();
const {
Suspender: SuspenderOne,
suspended: suspendedOne
} = createSuspender();
const {
Suspender: SuspenderTwo,
suspended: suspendedTwo
} = createSuspender();

const promise = renderToStringAsync(
<ul>
Expand All @@ -77,23 +109,121 @@ describe('Async renderToString', () => {
</ul>
);

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

suspendedOne.resolve();
suspendedTwo.resolve();

const rendered = await promise;

expect(rendered).to.equal(expected);
});

it('should render JSX with nested suspense boundaries containing multiple suspending components', async () => {
const {
Suspender: SuspenderOne,
suspended: suspendedOne
} = createSuspender();
const {
Suspender: SuspenderTwo,
suspended: suspendedTwo
} = createSuspender();
const {
Suspender: SuspenderThree,
suspended: suspendedThree
} = createSuspender('three');

const promise = renderToStringAsync(
<ul>
<Suspense fallback={null}>
<SuspenderOne>
<li>one</li>
<Suspense fallback={null}>
<SuspenderTwo>
<li>two</li>
</SuspenderTwo>
<SuspenderThree>
<li>three</li>
</SuspenderThree>
</Suspense>
<li>four</li>
</SuspenderOne>
</Suspense>
</ul>
);

const expected = `<ul><!-- $s --><li>one</li><!-- $s --><li>two</li><!-- /$s --><!-- $s --><li>three</li><!-- /$s --><li>four</li><!-- /$s --></ul>`;

suspendedOne.resolve();
suspendedTwo.resolve();
await wait(0);
suspendedThree.resolve();

const rendered = await promise;

expect(rendered).to.equal(expected);
});

it.only('should render JSX with deeply nested suspense boundaries', async () => {
const {
Suspender: SuspenderOne,
suspended: suspendedOne
} = createSuspender();
const {
Suspender: SuspenderTwo,
suspended: suspendedTwo
} = createSuspender();
const {
Suspender: SuspenderThree,
suspended: suspendedThree
} = createSuspender();

const promise = renderToStringAsync(
<ul>
<Suspense fallback={null}>
<SuspenderOne>
<li>one</li>
<Suspense fallback={null}>
<SuspenderTwo>
<li>two</li>
<Suspense fallback={null}>
<SuspenderThree>
<li>three</li>
</SuspenderThree>
</Suspense>
</SuspenderTwo>
</Suspense>
<li>four</li>
</SuspenderOne>
</Suspense>
</ul>
);

const expected = `<ul><!-- $s --><li>one</li><!-- $s --><li>two</li><!-- $s --><li>three</li><!-- /$s --><!-- /$s --><li>four</li><!-- /$s --></ul>`;

suspendedOne.resolve();
suspendedTwo.resolve();
await wait(0);
suspendedThree.resolve();

const rendered = await promise;

expect(rendered).to.equal(expected);
});

it('should render JSX with multiple suspended direct children within a single suspense boundary', async () => {
const { Suspender: SuspenderOne, suspended: suspendedOne } =
createSuspender();
const { Suspender: SuspenderTwo, suspended: suspendedTwo } =
createSuspender();
const { Suspender: SuspenderThree, suspended: suspendedThree } =
createSuspender();
const {
Suspender: SuspenderOne,
suspended: suspendedOne
} = createSuspender();
const {
Suspender: SuspenderTwo,
suspended: suspendedTwo
} = createSuspender();
const {
Suspender: SuspenderThree,
suspended: suspendedThree
} = createSuspender();

const promise = renderToStringAsync(
<ul>
Expand All @@ -113,7 +243,7 @@ describe('Async renderToString', () => {
</ul>
);

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

suspendedOne.resolve();
suspendedTwo.resolve();
Expand Down Expand Up @@ -173,6 +303,6 @@ describe('Async renderToString', () => {

suspended.resolve();
const rendered = await promise;
expect(rendered).to.equal('<p>ok</p>');
expect(rendered).to.equal('<!-- $s --><p>ok</p><!-- /$s -->');
});
});

0 comments on commit b0994fa

Please sign in to comment.