diff --git a/.changeset/grumpy-bobcats-rush.md b/.changeset/grumpy-bobcats-rush.md
new file mode 100644
index 000000000000..c8b147fa570c
--- /dev/null
+++ b/.changeset/grumpy-bobcats-rush.md
@@ -0,0 +1,5 @@
+---
+'@astrojs/solid-js': patch
+---
+
+Fix view transition state persistence
diff --git a/packages/astro/e2e/fixtures/view-transitions/astro.config.mjs b/packages/astro/e2e/fixtures/view-transitions/astro.config.mjs
index b7cfd434abb4..f406b0e0cd72 100644
--- a/packages/astro/e2e/fixtures/view-transitions/astro.config.mjs
+++ b/packages/astro/e2e/fixtures/view-transitions/astro.config.mjs
@@ -1,6 +1,7 @@
import nodejs from '@astrojs/node';
import react from '@astrojs/react';
import svelte from '@astrojs/svelte';
+import solidjs from '@astrojs/solid-js';
import vue from '@astrojs/vue';
import { defineConfig } from 'astro/config';
@@ -8,7 +9,11 @@ import { defineConfig } from 'astro/config';
export default defineConfig({
output: 'hybrid',
adapter: nodejs({ mode: 'standalone' }),
- integrations: [react(),vue(),svelte()],
+ integrations: [react( {
+ exclude: ['**/solid/**'],
+ }),vue(),svelte(),solidjs({
+ include: ['**/solid/**'],
+ })],
redirects: {
'/redirect-two': '/two',
'/redirect-external': 'http://example.com/',
diff --git a/packages/astro/e2e/fixtures/view-transitions/package.json b/packages/astro/e2e/fixtures/view-transitions/package.json
index 48cf30a9ae4b..08aae1afcf10 100644
--- a/packages/astro/e2e/fixtures/view-transitions/package.json
+++ b/packages/astro/e2e/fixtures/view-transitions/package.json
@@ -7,10 +7,12 @@
"@astrojs/react": "workspace:*",
"@astrojs/svelte": "workspace:*",
"@astrojs/vue": "workspace:*",
+ "@astrojs/solid-js": "workspace:*",
"astro": "workspace:*",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"svelte": "^4.2.19",
- "vue": "^3.5.3"
+ "vue": "^3.5.3",
+ "solid-js": "^1.8.0"
}
}
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/components/solid/Counter.jsx b/packages/astro/e2e/fixtures/view-transitions/src/components/solid/Counter.jsx
new file mode 100644
index 000000000000..13c8dd1935d1
--- /dev/null
+++ b/packages/astro/e2e/fixtures/view-transitions/src/components/solid/Counter.jsx
@@ -0,0 +1,19 @@
+import {createSignal} from "solid-js";
+
+export default function Counter(props) {
+ const [count, setCount] = createSignal(0);
+ const add = () => setCount(count() + 1);
+ const subtract = () => setCount(count() - 1);
+
+ return (
+ <>
+
+
+
+
{props.prefix ?? ''}{count()}{props.postfix ?? ""}
+
+
+ {props.children}
+ >
+ );
+}
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/island-solid-one.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/island-solid-one.astro
new file mode 100644
index 000000000000..92a58eaa7ce0
--- /dev/null
+++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/island-solid-one.astro
@@ -0,0 +1,11 @@
+---
+import Layout from '../components/Layout.astro';
+import Counter from '../components/solid/Counter.jsx';
+export const prerender = false;
+
+---
+
+ Page 1
+ go to 2
+
+
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/island-solid-two.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/island-solid-two.astro
new file mode 100644
index 000000000000..3d17f32b3950
--- /dev/null
+++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/island-solid-two.astro
@@ -0,0 +1,11 @@
+---
+import Layout from '../components/Layout.astro';
+import Counter from '../components/solid/Counter.jsx';
+export const prerender = false;
+
+---
+
+ Page 2
+ go to 1
+
+
diff --git a/packages/astro/e2e/view-transitions.test.js b/packages/astro/e2e/view-transitions.test.js
index f1ced26c7d6f..d5f78f1de08b 100644
--- a/packages/astro/e2e/view-transitions.test.js
+++ b/packages/astro/e2e/view-transitions.test.js
@@ -544,6 +544,31 @@ test.describe('View Transitions', () => {
await expect(pageTitle).toHaveText('Island 2');
});
+ test('Solid Islands can persist using transition:persist', async ({ page, astro }) => {
+ // Go to page 1
+ await page.goto(astro.resolveUrl('/island-solid-one'));
+ let cnt = page.locator('.counter pre');
+ await expect(cnt).toHaveText('A0');
+
+ await page.click('.increment');
+ await expect(cnt).toHaveText('A1');
+
+ // Navigate to page 2
+ await page.click('#click-two');
+ let p = page.locator('#island-two');
+ await expect(p).toBeVisible();
+ cnt = page.locator('.counter pre');
+ // Count should remain, but the prefix should be updated
+ await expect(cnt).toHaveText('B1!');
+
+ await page.click('#click-one');
+ p = page.locator('#island-one');
+ await expect(p).toBeVisible();
+ cnt = page.locator('.counter pre');
+ // Count should remain, but the postfix should be removed again (to test unsetting props)
+ await expect(cnt).toHaveText('A1');
+ });
+
test('Vue Islands can persist using transition:persist', async ({ page, astro }) => {
// Go to page 1
await page.goto(astro.resolveUrl('/island-vue-one'));
diff --git a/packages/integrations/solid/src/client.ts b/packages/integrations/solid/src/client.ts
index a47c5310db23..f2020bb564b9 100644
--- a/packages/integrations/solid/src/client.ts
+++ b/packages/integrations/solid/src/client.ts
@@ -1,10 +1,12 @@
import { Suspense } from 'solid-js';
+import { createStore, reconcile } from 'solid-js/store';
import { createComponent, hydrate, render } from 'solid-js/web';
+const alreadyInitializedElements = new WeakMap();
+
export default (element: HTMLElement) =>
(Component: any, props: any, slotted: any, { client }: { client: string }) => {
if (!element.hasAttribute('ssr')) return;
-
const isHydrate = client !== 'only';
const bootstrap = isHydrate ? hydrate : render;
@@ -32,31 +34,44 @@ export default (element: HTMLElement) =>
const { default: children, ...slots } = _slots;
const renderId = element.dataset.solidRenderId;
+ if (alreadyInitializedElements.has(element)) {
+ // update the mounted component
+ alreadyInitializedElements.get(element)!(
+ // reconcile will make sure to apply as little updates as possible, and also remove missing values w/o breaking reactivity
+ reconcile({
+ ...props,
+ ...slots,
+ children,
+ }),
+ );
+ } else {
+ const [store, setStore] = createStore({
+ ...props,
+ ...slots,
+ children,
+ });
+ // store the function to update the current mounted component
+ alreadyInitializedElements.set(element, setStore);
- const dispose = bootstrap(
- () => {
- const inner = () =>
- createComponent(Component, {
- ...props,
- ...slots,
- children,
- });
+ const dispose = bootstrap(
+ () => {
+ const inner = () => createComponent(Component, store);
- if (isHydrate) {
- return createComponent(Suspense, {
- get children() {
- return inner();
- },
- });
- } else {
- return inner();
- }
- },
- element,
- {
- renderId,
- },
- );
-
- element.addEventListener('astro:unmount', () => dispose(), { once: true });
+ if (isHydrate) {
+ return createComponent(Suspense, {
+ get children() {
+ return inner();
+ },
+ });
+ } else {
+ return inner();
+ }
+ },
+ element,
+ {
+ renderId,
+ },
+ );
+ element.addEventListener('astro:unmount', () => dispose(), { once: true });
+ }
};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6c5752503857..9acbdc8aa572 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1721,6 +1721,9 @@ importers:
'@astrojs/react':
specifier: workspace:*
version: link:../../../../integrations/react
+ '@astrojs/solid-js':
+ specifier: workspace:*
+ version: link:../../../../integrations/solid
'@astrojs/svelte':
specifier: workspace:*
version: link:../../../../integrations/svelte
@@ -1736,6 +1739,9 @@ importers:
react-dom:
specifier: ^18.3.1
version: 18.3.1(react@18.3.1)
+ solid-js:
+ specifier: ^1.8.0
+ version: 1.8.22
svelte:
specifier: ^4.2.19
version: 4.2.19