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 Jan 15, 2025
1 parent b2050f4 commit b53f368
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 11 deletions.
41 changes: 35 additions & 6 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const EMPTY_ARR = [];
const isArray = Array.isArray;
const assign = Object.assign;
const EMPTY_STR = '';
const BEGIN_SUSPENSE_DENOMINATOR = '<!-- $s -->';
const END_SUSPENSE_DENOMINATOR = '<!-- /$s -->';

// Global state for the current render pass
let beforeDiff, afterDiff, renderHook, ummountHook;
Expand Down Expand Up @@ -372,7 +374,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 @@ -403,6 +410,7 @@ function _renderToString(
selectValue,
vnode,
asyncMode,
false,
renderer
);
} catch (err) {
Expand Down Expand Up @@ -475,6 +483,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 @@ -503,7 +526,7 @@ function _renderToString(

const renderNestedChildren = () => {
try {
return _renderToString(
const result = _renderToString(
rendered,
context,
isSvgMode,
Expand All @@ -512,21 +535,27 @@ 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(
() => {
const result = _renderToString(
rendered,
context,
isSvgMode,
selectValue,
vnode,
asyncMode,
renderer
),
renderNestedChildren
)
return vnode._suspended
? BEGIN_SUSPENSE_DENOMINATOR + result + END_SUSPENSE_DENOMINATOR
: result;
}, renderNestedChildren
);
}
};
Expand Down
126 changes: 121 additions & 5 deletions test/compat/async.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { h, Fragment } from 'preact';
import { Suspense, useId, lazy, createContext } 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 Down Expand Up @@ -49,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 Down Expand Up @@ -85,7 +109,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 @@ -95,6 +119,98 @@ describe('Async renderToString', () => {
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,
Expand Down Expand Up @@ -127,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 @@ -187,7 +303,7 @@ 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 -->');
});

it('should work with an in-render suspension', async () => {
Expand Down

0 comments on commit b53f368

Please sign in to comment.