diff --git a/.changeset/lovely-nails-cough.md b/.changeset/lovely-nails-cough.md
new file mode 100644
index 000000000000..4aecb48d141d
--- /dev/null
+++ b/.changeset/lovely-nails-cough.md
@@ -0,0 +1,26 @@
+---
+"@astrojs/react": minor
+"astro": minor
+---
+
+Changes the default behavior of `transition:persist` to update the props of persisted islands upon navigation. Also adds a new view transitions option `transition:persist-props` (default: `false`) to prevent props from updating as needed.
+
+Islands which have the `transition:persist` property to keep their state when using the `` router will now have their props updated upon navigation. This is useful in cases where the component relies on page-specific props, such as the current page title, which should update upon navigation.
+
+For example, the component below is set to persist across navigation. This component receives a `products` props and might have some internal state, such as which filters are applied:
+
+```astro
+
+```
+
+Upon navigation, this component persists, but the desired `products` might change, for example if you are visiting a category of products, or you are performing a search.
+
+Previously the props would not change on navigation, and your island would have to handle updating them externally, such as with API calls.
+
+With this change the props are now updated, while still preserving state.
+
+You can override this new default behavior on a per-component basis using `transition:persist-props=true` to persist both props and state during navigation:
+
+```astro
+
+```
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/components/Island.jsx b/packages/astro/e2e/fixtures/view-transitions/src/components/Island.jsx
index 734e2011b25b..541ca6026e19 100644
--- a/packages/astro/e2e/fixtures/view-transitions/src/components/Island.jsx
+++ b/packages/astro/e2e/fixtures/view-transitions/src/components/Island.jsx
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import './Island.css';
import { indirect} from './css.js';
-export default function Counter({ children, count: initialCount, id }) {
+export default function Counter({ children, count: initialCount, id, page }) {
const [count, setCount] = useState(initialCount);
const add = () => setCount((i) => i + 1);
const subtract = () => setCount((i) => i - 1);
@@ -10,6 +10,7 @@ export default function Counter({ children, count: initialCount, id }) {
return (
<>
+
{page}
{count}
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/island-one.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/island-one.astro
index 89822a01be93..883d567a118f 100644
--- a/packages/astro/e2e/fixtures/view-transitions/src/pages/island-one.astro
+++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/island-one.astro
@@ -1,9 +1,13 @@
---
import Layout from '../components/Layout.astro';
import Island from '../components/Island.jsx';
+export const prerender = false;
+
+const persistProps = Astro.url.searchParams.has('persist');
---
Page 1
go to 2
-
+
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/island-two.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/island-two.astro
index 3841ca8970c3..37912591cb57 100644
--- a/packages/astro/e2e/fixtures/view-transitions/src/pages/island-two.astro
+++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/island-two.astro
@@ -5,5 +5,5 @@ import Island from '../components/Island.jsx';
Page 2
go to 1
-
+
diff --git a/packages/astro/e2e/view-transitions.test.js b/packages/astro/e2e/view-transitions.test.js
index d90575c52286..b963bf2fd7f4 100644
--- a/packages/astro/e2e/view-transitions.test.js
+++ b/packages/astro/e2e/view-transitions.test.js
@@ -543,6 +543,38 @@ test.describe('View Transitions', () => {
cnt = page.locator('.counter pre');
// Count should remain
await expect(cnt).toHaveText('6');
+
+ // Props should have changed
+ const pageTitle = page.locator('.page');
+ await expect(pageTitle).toHaveText('Island 2');
+ });
+
+ test('transition:persist-props prevents props from changing', async ({ page, astro }) => {
+ // Go to page 1
+ await page.goto(astro.resolveUrl('/island-one?persist'));
+
+ // Navigate to page 2
+ await page.click('#click-two');
+ const p = page.locator('#island-two');
+ await expect(p).toBeVisible();
+
+ // Props should have changed
+ const pageTitle = page.locator('.page');
+ await expect(pageTitle).toHaveText('Island 1');
+ });
+
+ test('transition:persist-props=false makes props update', async ({ page, astro }) => {
+ // Go to page 2
+ await page.goto(astro.resolveUrl('/island-two'));
+
+ // Navigate to page 1
+ await page.click('#click-one');
+ const p = page.locator('#island-one');
+ await expect(p).toBeVisible();
+
+ // Props should have changed
+ const pageTitle = page.locator('.page');
+ await expect(pageTitle).toHaveText('Island 1');
});
test('Scripts are only executed once', async ({ page, astro }) => {
diff --git a/packages/astro/package.json b/packages/astro/package.json
index 42ee8a741531..b1bbf0b2f434 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -114,7 +114,7 @@
"test:node": "astro-scripts test \"test/**/*.test.js\""
},
"dependencies": {
- "@astrojs/compiler": "^2.5.3",
+ "@astrojs/compiler": "^2.7.0",
"@astrojs/internal-helpers": "workspace:*",
"@astrojs/markdown-remark": "workspace:*",
"@astrojs/telemetry": "workspace:*",
diff --git a/packages/astro/src/runtime/server/hydration.ts b/packages/astro/src/runtime/server/hydration.ts
index 4c8b71ba8843..28b5ff674e19 100644
--- a/packages/astro/src/runtime/server/hydration.ts
+++ b/packages/astro/src/runtime/server/hydration.ts
@@ -27,6 +27,7 @@ interface ExtractedProps {
const transitionDirectivesToCopyOnIsland = Object.freeze([
'data-astro-transition-scope',
'data-astro-transition-persist',
+ 'data-astro-transition-persist-props',
]);
// Used to extract the directives, aka `client:load` information about a component.
@@ -175,7 +176,7 @@ export async function generateHydrateScript(
);
transitionDirectivesToCopyOnIsland.forEach((name) => {
- if (props[name]) {
+ if (typeof props[name] !== 'undefined') {
island.props[name] = props[name];
}
});
diff --git a/packages/astro/src/transitions/router.ts b/packages/astro/src/transitions/router.ts
index 02e2d69dfd20..817ada55c211 100644
--- a/packages/astro/src/transitions/router.ts
+++ b/packages/astro/src/transitions/router.ts
@@ -306,6 +306,11 @@ async function updateDOM(
}
};
+ const shouldCopyProps = (el: HTMLElement): boolean => {
+ const persistProps = el.dataset.astroTransitionPersistProps;
+ return persistProps == null || persistProps === 'false';
+ }
+
const defaultSwap = (beforeSwapEvent: TransitionBeforeSwapEvent) => {
// swap attributes of the html element
// - delete all attributes from the current document
@@ -366,6 +371,11 @@ async function updateDOM(
// The element exists in the new page, replace it with the element
// from the old page so that state is preserved.
newEl.replaceWith(el);
+ // For islands, copy over the props to allow them to re-render
+ if(newEl.localName === 'astro-island' && shouldCopyProps(el as HTMLElement)) {
+ el.setAttribute('ssr', '');
+ el.setAttribute('props', newEl.getAttribute('props')!);
+ }
}
}
restoreFocus(savedFocus);
diff --git a/packages/integrations/react/client.js b/packages/integrations/react/client.js
index 90e4ceb837b5..cfe46a919223 100644
--- a/packages/integrations/react/client.js
+++ b/packages/integrations/react/client.js
@@ -53,6 +53,17 @@ function getChildren(childString, experimentalReactChildren) {
}
}
+// Keep a map of roots so we can reuse them on re-renders
+let rootMap = new WeakMap();
+const getOrCreateRoot = (element, creator) => {
+ let root = rootMap.get(element);
+ if(!root) {
+ root = creator();
+ rootMap.set(element, root);
+ }
+ return root;
+};
+
export default (element) =>
(Component, props, { default: children, ...slotted }, { client }) => {
if (!element.hasAttribute('ssr')) return;
@@ -75,14 +86,20 @@ export default (element) =>
}
if (client === 'only') {
return startTransition(() => {
- const root = createRoot(element);
+ const root = getOrCreateRoot(element, () => {
+ const r = createRoot(element);
+ element.addEventListener('astro:unmount', () => r.unmount(), { once: true });
+ return r;
+ });
root.render(componentEl);
- element.addEventListener('astro:unmount', () => root.unmount(), { once: true });
});
}
startTransition(() => {
- const root = hydrateRoot(element, componentEl, renderOptions);
+ const root = getOrCreateRoot(element, () => {
+ const r = hydrateRoot(element, componentEl, renderOptions);
+ element.addEventListener('astro:unmount', () => r.unmount(), { once: true });
+ return r;
+ });
root.render(componentEl);
- element.addEventListener('astro:unmount', () => root.unmount(), { once: true });
});
};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b8e8d09977b6..130057300a0d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -498,8 +498,8 @@ importers:
packages/astro:
dependencies:
'@astrojs/compiler':
- specifier: ^2.5.3
- version: 2.5.3
+ specifier: ^2.7.0
+ version: 2.7.0
'@astrojs/internal-helpers':
specifier: workspace:*
version: link:../internal-helpers
@@ -5529,8 +5529,8 @@ packages:
/@astrojs/compiler@1.8.2:
resolution: {integrity: sha512-o/ObKgtMzl8SlpIdzaxFnt7SATKPxu4oIP/1NL+HDJRzxfJcAkOTAb/ZKMRyULbz4q+1t2/DAebs2Z1QairkZw==}
- /@astrojs/compiler@2.5.3:
- resolution: {integrity: sha512-jzj01BRv/fmo+9Mr2FhocywGzEYiyiP2GVHje1ziGNU6c97kwhYGsnvwMkHrncAy9T9Vi54cjaMK7UE4ClX4vA==}
+ /@astrojs/compiler@2.7.0:
+ resolution: {integrity: sha512-XpC8MAaWjD1ff6/IfkRq/5k1EFj6zhCNqXRd5J43SVJEBj/Bsmizkm8N0xOYscGcDFQkRgEw6/eKnI5x/1l6aA==}
/@astrojs/language-server@2.5.5(prettier-plugin-astro@0.12.3)(prettier@3.1.1)(typescript@5.2.2):
resolution: {integrity: sha512-hk7a8S7bpf//BOA6mMjiYqi/eiYtGPfUfw59eVXdutdRFdwDHtu4jcsLu43ZaId56pAcE8qFjIvJxySvzcxiUA==}
@@ -5544,7 +5544,7 @@ packages:
prettier-plugin-astro:
optional: true
dependencies:
- '@astrojs/compiler': 2.5.3
+ '@astrojs/compiler': 2.7.0
'@jridgewell/sourcemap-codec': 1.4.15
'@volar/kit': 1.10.10(typescript@5.2.2)
'@volar/language-core': 1.10.10
@@ -5580,7 +5580,7 @@ packages:
prettier-plugin-astro:
optional: true
dependencies:
- '@astrojs/compiler': 2.5.3
+ '@astrojs/compiler': 2.7.0
'@jridgewell/sourcemap-codec': 1.4.15
'@volar/kit': 2.0.4(typescript@5.3.2)
'@volar/language-core': 2.0.4