Skip to content

Commit 8e8e8ac

Browse files
authored
Use suspense boundary vnode as parent for subtree re-render (#408)
* use suspense boundary vnode as parent for re-render * update internal types for renderChild * add changeset * add test to check for VNode circular references inside suspense boundary renderChild * add useId with Suspense test * remove unused import
1 parent 2afaf31 commit 8e8e8ac

File tree

7 files changed

+97
-9
lines changed

7 files changed

+97
-9
lines changed

.changeset/friendly-numbers-hang.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'preact-render-to-string': patch
3+
---
4+
5+
Fix issue where subtree re-render for Suspense boundaries caused a circular reference in the VNode's parent

src/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,13 +480,13 @@ function _renderToString(
480480
return str;
481481
} catch (error) {
482482
if (!asyncMode && renderer && renderer.onError) {
483-
let res = renderer.onError(error, vnode, (child) =>
483+
let res = renderer.onError(error, vnode, (child, parent) =>
484484
_renderToString(
485485
child,
486486
context,
487487
isSvgMode,
488488
selectValue,
489-
vnode,
489+
parent,
490490
asyncMode,
491491
renderer
492492
)

src/internal.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ComponentChildren, VNode } from 'preact';
1+
import { ComponentChildren, ComponentChild, VNode } from 'preact';
22

33
interface Suspended {
44
id: string;
@@ -15,7 +15,7 @@ interface RendererErrorHandler {
1515
this: RendererState,
1616
error: any,
1717
vnode: VNode<{ fallback: any }>,
18-
renderChild: (child: ComponentChildren) => string
18+
renderChild: (child: ComponentChildren, parent: ComponentChild) => string
1919
): string | undefined;
2020
}
2121

src/lib/chunked.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ function handleError(error, vnode, renderChild) {
7575
const promise = error.then(
7676
() => {
7777
if (abortSignal && abortSignal.aborted) return;
78-
const child = renderChild(vnode.props.children);
78+
const child = renderChild(vnode.props.children, vnode);
7979
if (child) this.onWrite(createSubtree(id, child));
8080
},
8181
// TODO: Abort and send hydration code snippet to client

test/compat/render-chunked.test.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { h } from 'preact';
22
import { expect } from 'chai';
33
import { Suspense } from 'preact/compat';
4+
import { useId } from 'preact/hooks';
45
import { renderToChunks } from '../../src/lib/chunked';
56
import { createSubtree, createInitScript } from '../../src/lib/client';
67
import { createSuspender } from '../utils';
8+
import { VNODE, PARENT } from '../../src/lib/constants';
79

810
describe('renderToChunks', () => {
911
it('should render non-suspended JSX in one go', async () => {
@@ -66,4 +68,85 @@ describe('renderToChunks', () => {
6668
'</div>'
6769
]);
6870
});
71+
72+
it('should encounter no circular references when rendering a suspense boundary subtree', async () => {
73+
const { Suspender, suspended } = createSuspender();
74+
75+
const visited = new Set();
76+
let circular = false;
77+
78+
function CircularReferenceCheck() {
79+
let root = this[VNODE];
80+
while (root !== null && root[PARENT] !== null) {
81+
if (visited.has(root)) {
82+
// Can't throw an error here, _catchError handler will also loop infinitely
83+
circular = true;
84+
break;
85+
}
86+
visited.add(root);
87+
root = root[PARENT];
88+
}
89+
return <p>it works</p>;
90+
}
91+
92+
const result = [];
93+
const promise = renderToChunks(
94+
<div>
95+
<Suspense fallback="loading...">
96+
<Suspender>
97+
<CircularReferenceCheck />
98+
</Suspender>
99+
</Suspense>
100+
</div>,
101+
{ onWrite: (s) => result.push(s) }
102+
);
103+
104+
suspended.resolve();
105+
await promise;
106+
107+
if (circular) {
108+
throw new Error('CircularReference');
109+
}
110+
111+
expect(result).to.deep.equal([
112+
'<div><!--preact-island:16-->loading...<!--/preact-island:16--></div>',
113+
'<div hidden>',
114+
createInitScript(1),
115+
createSubtree('16', '<p>it works</p>'),
116+
'</div>'
117+
]);
118+
});
119+
120+
it('should support using useId hooks inside a suspense boundary', async () => {
121+
const { Suspender, suspended } = createSuspender();
122+
123+
function ComponentWithId() {
124+
const id = useId();
125+
return <p>id: {id}</p>;
126+
}
127+
128+
const result = [];
129+
const promise = renderToChunks(
130+
<div>
131+
<ComponentWithId />
132+
<Suspense fallback="loading...">
133+
<Suspender>
134+
<ComponentWithId />
135+
</Suspender>
136+
</Suspense>
137+
</div>,
138+
{ onWrite: (s) => result.push(s) }
139+
);
140+
141+
suspended.resolve();
142+
await promise;
143+
144+
expect(result).to.deep.equal([
145+
'<div><p>id: P0-0</p><!--preact-island:24-->loading...<!--/preact-island:24--></div>',
146+
'<div hidden>',
147+
createInitScript(1),
148+
createSubtree('24', '<p>id: P0-0</p>'),
149+
'</div>'
150+
]);
151+
});
69152
});

test/compat/stream-node.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,10 @@ describe('renderToPipeableStream', () => {
6666
const result = await sink.promise;
6767

6868
expect(result).to.deep.equal([
69-
'<div><!--preact-island:17-->loading...<!--/preact-island:17--></div>',
69+
'<div><!--preact-island:33-->loading...<!--/preact-island:33--></div>',
7070
'<div hidden>',
7171
createInitScript(),
72-
createSubtree('17', '<p>it works</p>'),
72+
createSubtree('33', '<p>it works</p>'),
7373
'</div>'
7474
]);
7575
});

test/compat/stream.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,10 @@ describe('renderToReadableStream', () => {
8282
const result = await sink.promise;
8383

8484
expect(result).to.deep.equal([
85-
'<div><!--preact-island:24-->loading...<!--/preact-island:24--></div>',
85+
'<div><!--preact-island:40-->loading...<!--/preact-island:40--></div>',
8686
'<div hidden>',
8787
createInitScript(),
88-
createSubtree('24', '<p>it works</p>'),
88+
createSubtree('40', '<p>it works</p>'),
8989
'</div>'
9090
]);
9191
});

0 commit comments

Comments
 (0)