diff --git a/.changeset/chilly-badgers-push.md b/.changeset/chilly-badgers-push.md
new file mode 100644
index 000000000..2bbd12f0d
--- /dev/null
+++ b/.changeset/chilly-badgers-push.md
@@ -0,0 +1,21 @@
+---
+'@astrojs/solid-js': major
+---
+
+Render SolidJS components using [renderToStringAsync](https://www.solidjs.com/docs/latest#rendertostringasync).
+
+This changes the renderer of SolidJS components from renderToString to renderToStringAsync.
+
+The server render phase will now wait for Suspense boundaries to resolve instead of immediately rendering the [Suspense](https://www.solidjs.com/docs/latest#suspense) fallback.
+
+If your SolidJS component uses APIs such as [lazy](https://www.solidjs.com/docs/latest#lazy) or [createResource](https://www.solidjs.com/docs/latest#createresource), these functions may now be called on the server side.
+
+This increases the power of the SolidJS integration. For example, server-only SolidJS components could now call async functions directly using the `createResource` API, like loading data from another API or using the async Astro Image function `getImage()`. It is unlikely that a server only component would make use of Suspense until now, so this should not be a breaking change for server-only components.
+
+This could be a breaking change if:
+
+- the component has a hydrating directive like `client:load`, and
+- the component uses Suspense APIs like `lazy` or `createResource`, and
+- the component uses Suspense fallback with the intention for it to be a server-side fallback
+
+In this case, instead of relying on Suspense as a server-side fallback, use APIs like [isServer](https://www.solidjs.com/docs/latest/api#isserver) or `onMount()` to detect server mode and render the server fallback without using Suspense.
diff --git a/packages/astro/e2e/shared-component-tests.js b/packages/astro/e2e/shared-component-tests.js
index e8ec273fd..ccce25b0b 100644
--- a/packages/astro/e2e/shared-component-tests.js
+++ b/packages/astro/e2e/shared-component-tests.js
@@ -1,7 +1,7 @@
import { expect } from '@playwright/test';
import { scrollToElement, testFactory, waitForHydrate } from './test-utils.js';
-export function prepareTestFactory(opts) {
+export function prepareTestFactory(opts, { canReplayClicks = false } = {}) {
const test = testFactory(opts);
let devServer;
@@ -104,7 +104,16 @@ export function prepareTestFactory(opts) {
await waitForHydrate(page, counter);
await inc.click();
- await expect(count, 'count incremented by 1').toHaveText('1');
+
+ if (canReplayClicks) {
+ // SolidJS has a hydration script that automatically captures
+ // and replays click and input events on Hydration:
+ // https://www.solidjs.com/docs/latest#hydrationscript
+ // so in total there are two click events.
+ await expect(count, 'count incremented by 2').toHaveText('2');
+ } else {
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ }
});
test('client:only', async ({ page, astro }) => {
diff --git a/packages/astro/e2e/solid-component.test.js b/packages/astro/e2e/solid-component.test.js
index 7a195c9b1..81e6894e8 100644
--- a/packages/astro/e2e/solid-component.test.js
+++ b/packages/astro/e2e/solid-component.test.js
@@ -1,6 +1,11 @@
import { prepareTestFactory } from './shared-component-tests.js';
-const { test, createTests } = prepareTestFactory({ root: './fixtures/solid-component/' });
+const { test, createTests } = prepareTestFactory(
+ { root: './fixtures/solid-component/' },
+ {
+ canReplayClicks: true,
+ }
+);
const config = {
componentFilePath: './src/components/SolidComponent.jsx',
diff --git a/packages/astro/test/fixtures/solid-component/src/components/Counter.jsx b/packages/astro/test/fixtures/solid-component/src/components/Counter.jsx
new file mode 100644
index 000000000..648a6af15
--- /dev/null
+++ b/packages/astro/test/fixtures/solid-component/src/components/Counter.jsx
@@ -0,0 +1,34 @@
+// Based on reproduction from https://github.com/withastro/astro/issues/6912
+
+import { For, Match, Switch } from 'solid-js';
+
+export default function Counter(props) {
+ return (
+
+ {(page) => {
+ return (
+
+
+
+
+
+
+
+
+ );
+ }}
+
+ );
+}
diff --git a/packages/astro/test/fixtures/solid-component/src/components/LazyCounter.jsx b/packages/astro/test/fixtures/solid-component/src/components/LazyCounter.jsx
new file mode 100644
index 000000000..27ff06246
--- /dev/null
+++ b/packages/astro/test/fixtures/solid-component/src/components/LazyCounter.jsx
@@ -0,0 +1,5 @@
+// Based on reproduction from https://github.com/withastro/astro/issues/6912
+
+import { lazy } from 'solid-js';
+
+export const LazyCounter = lazy(() => import('./Counter'));
diff --git a/packages/astro/test/fixtures/solid-component/src/components/async-components.jsx b/packages/astro/test/fixtures/solid-component/src/components/async-components.jsx
new file mode 100644
index 000000000..1b3b0db69
--- /dev/null
+++ b/packages/astro/test/fixtures/solid-component/src/components/async-components.jsx
@@ -0,0 +1,55 @@
+import { createResource, ErrorBoundary } from 'solid-js';
+
+// It may be good to try short and long sleep times.
+// But short is faster for testing.
+const SLEEP_MS = 10;
+
+const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
+
+export function AsyncComponent(props) {
+ const [data] = createResource(async () => {
+ // console.log("Start rendering async component " + props.title);
+ await sleep(props.delay ?? SLEEP_MS);
+ // console.log("Finish rendering async component " + props.title);
+ return 'async_result_from_async_component';
+ });
+
+ return (
+
actions.refetch()}>
+ {data()}
+ {/* NOTE: The props.children are intentionally commented out
+ to simulate a situation where hydration script might not
+ be injected in the right spot. */}
+ {/* {props.children} */}
+