diff --git a/.changeset/friendly-numbers-hang.md b/.changeset/friendly-numbers-hang.md new file mode 100644 index 0000000..764bcd8 --- /dev/null +++ b/.changeset/friendly-numbers-hang.md @@ -0,0 +1,5 @@ +--- +'preact-render-to-string': patch +--- + +Fix issue where subtree re-render for Suspense boundaries caused a circular reference in the VNode's parent diff --git a/src/index.js b/src/index.js index a617432..4c7fe19 100644 --- a/src/index.js +++ b/src/index.js @@ -480,13 +480,13 @@ function _renderToString( return str; } catch (error) { if (!asyncMode && renderer && renderer.onError) { - let res = renderer.onError(error, vnode, (child) => + let res = renderer.onError(error, vnode, (child, parent) => _renderToString( child, context, isSvgMode, selectValue, - vnode, + parent, asyncMode, renderer ) diff --git a/src/internal.d.ts b/src/internal.d.ts index 8d2959f..b4bbec8 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -1,4 +1,4 @@ -import { ComponentChildren, VNode } from 'preact'; +import { ComponentChildren, ComponentChild, VNode } from 'preact'; interface Suspended { id: string; @@ -15,7 +15,7 @@ interface RendererErrorHandler { this: RendererState, error: any, vnode: VNode<{ fallback: any }>, - renderChild: (child: ComponentChildren) => string + renderChild: (child: ComponentChildren, parent: ComponentChild) => string ): string | undefined; } diff --git a/src/lib/chunked.js b/src/lib/chunked.js index 38e6a26..cd1686d 100644 --- a/src/lib/chunked.js +++ b/src/lib/chunked.js @@ -75,7 +75,7 @@ function handleError(error, vnode, renderChild) { const promise = error.then( () => { if (abortSignal && abortSignal.aborted) return; - const child = renderChild(vnode.props.children); + const child = renderChild(vnode.props.children, vnode); if (child) this.onWrite(createSubtree(id, child)); }, // TODO: Abort and send hydration code snippet to client diff --git a/test/compat/render-chunked.test.js b/test/compat/render-chunked.test.js index b043b8f..575e0f9 100644 --- a/test/compat/render-chunked.test.js +++ b/test/compat/render-chunked.test.js @@ -1,9 +1,11 @@ import { h } from 'preact'; import { expect } from 'chai'; import { Suspense } from 'preact/compat'; +import { useId } from 'preact/hooks'; import { renderToChunks } from '../../src/lib/chunked'; import { createSubtree, createInitScript } from '../../src/lib/client'; import { createSuspender } from '../utils'; +import { VNODE, PARENT } from '../../src/lib/constants'; describe('renderToChunks', () => { it('should render non-suspended JSX in one go', async () => { @@ -66,4 +68,85 @@ describe('renderToChunks', () => { '' ]); }); + + it('should encounter no circular references when rendering a suspense boundary subtree', async () => { + const { Suspender, suspended } = createSuspender(); + + const visited = new Set(); + let circular = false; + + function CircularReferenceCheck() { + let root = this[VNODE]; + while (root !== null && root[PARENT] !== null) { + if (visited.has(root)) { + // Can't throw an error here, _catchError handler will also loop infinitely + circular = true; + break; + } + visited.add(root); + root = root[PARENT]; + } + return
it works
; + } + + const result = []; + const promise = renderToChunks( +it works
'), + 'id: {id}
; + } + + const result = []; + const promise = renderToChunks( +id: P0-0
loading...id: P0-0
'), + 'it works
'), + createSubtree('33', 'it works
'), 'it works
'), + createSubtree('40', 'it works
'), '