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