Repository: preactjs/preact-render-to-string
Pull request #378 (2024-07-17 12:38:06)

Fix instance where renderToStringAsync returns a promise of a promise
Fixes https://github.com/preactjs/preact-render-to-string/issues/377

I have had this happen with a project I work on. This includes a minimal test reproduction of it.




changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: 0f7f1d07a031983e58fa4544e366d21b7894b585

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/ccouzens/preact-render-to-string/new/patch-1?filename=.changeset/hungry-cobras-retire.md&value=---%0A%22preact-render-to-string%22%3A%20patch%0A---%0A%0AFix%20instance%20where%20renderToStringAsync%20returns%20a%20promise%20of%20a%20promise%0A)


JoviDeCroock: Have you tried breaking it by increasing the nesting even further? Just wondering 😅 

Patch:
diff --git a/.changeset/famous-experts-decide.md b/.changeset/famous-experts-decide.md
new file mode 100644
index 0000000..476006a
--- /dev/null
+++ b/.changeset/famous-experts-decide.md
@@ -0,0 +1,5 @@
+---
+'preact-render-to-string': patch
+---
+
+Fix issue where preactRenderToString returns a promise of a promise
diff --git a/src/index.js b/src/index.js
index 9dec64a..743f25a 100644
--- a/src/index.js
+++ b/src/index.js
@@ -109,7 +109,7 @@ export async function renderToStringAsync(vnode, context) {
 	parent[CHILDREN] = [vnode];
 
 	try {
-		const rendered = _renderToString(
+		const rendered = await _renderToString(
 			vnode,
 			context || EMPTY_OBJ,
 			false,


Test patch:
diff --git a/test/compat/async.test.jsx b/test/compat/async.test.jsx
index 8b50c00..968c12b 100644
--- a/test/compat/async.test.jsx
+++ b/test/compat/async.test.jsx
@@ -1,6 +1,6 @@
 import { renderToStringAsync } from '../../src/index.js';
-import { h } from 'preact';
-import { Suspense, useId } from 'preact/compat';
+import { h, Fragment } from 'preact';
+import { Suspense, useId, lazy, createContext } from 'preact/compat';
 import { expect } from 'chai';
 import { createSuspender } from '../utils.jsx';
 
@@ -26,10 +26,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>
@@ -56,10 +60,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>
@@ -88,12 +96,18 @@ describe('Async renderToString', () => {
 	});
 
 	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>
@@ -175,4 +189,41 @@ describe('Async renderToString', () => {
 		const rendered = await promise;
 		expect(rendered).to.equal('<p>ok</p>');
 	});
+
+	it('should work with an in-render suspension', async () => {
+		const Context = createContext();
+
+		let c = 0;
+
+		const Fetcher = ({ children }) => {
+			c++;
+			if (c === 1) {
+				throw Promise.resolve();
+			}
+			return <Fragment>{children}</Fragment>;
+		};
+
+		const LazyComponent = lazy(
+			async () =>
+				function ImportedComponent() {
+					return <div>2</div>;
+				}
+		);
+
+		const LoadableComponent = ({}) => (
+			<Suspense fallback={'...loading'}>
+				<LazyComponent />
+			</Suspense>
+		);
+
+		const rendered = await renderToStringAsync(
+			<Context.Provider>
+				<Fetcher>
+					<LoadableComponent />
+				</Fetcher>
+			</Context.Provider>
+		);
+
+		expect(rendered).to.equal(`<div>2</div>`);
+	});
 });