summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Nate Moore <natemoo-re@users.noreply.github.com> 2023-08-29 09:30:11 -0500
committerGravatar GitHub <noreply@github.com> 2023-08-29 09:30:11 -0500
commit1f58a7a1bea6888868b689dac94801d554319b02 (patch)
treed211fa64f18e32ca0798e1ca93b96c2477eea322
parent9e021a91c57d10809f588dd47968fc0e7f8b4d5c (diff)
downloadastro-1f58a7a1bea6888868b689dac94801d554319b02.tar.gz
astro-1f58a7a1bea6888868b689dac94801d554319b02.tar.zst
astro-1f58a7a1bea6888868b689dac94801d554319b02.zip
Unmount framework components when islands are destroyed (#8264)
* fix(view-transitions): update persistence logic for improved unmount behavior * feat(astro): add `astro:unmount` event * feat(vue): automatically unmount islands * feat(react): automatically unmount islands * feat(react): automatically unmount islands * feat(solid): automatically dispose of islands * feat(svelte): automatically destroy of islands * feat(svelte): automatically destroy of islands * feat(solid): automatically dispose of islands * feat(preact): automatically unmount islands * chore: update changeset * fix: rebase issue * chore: add clarifying comment * chore: remove duplicate changeset * chore: add changeset
-rw-r--r--.changeset/ninety-boats-brake.md9
-rw-r--r--.changeset/perfect-socks-hammer.md5
-rw-r--r--packages/astro/components/ViewTransitions.astro8
-rw-r--r--packages/astro/src/runtime/server/astro-island.ts6
-rw-r--r--packages/integrations/preact/src/client.ts31
-rw-r--r--packages/integrations/react/client-v17.js11
-rw-r--r--packages/integrations/react/client.js10
-rw-r--r--packages/integrations/solid/src/client.ts6
-rw-r--r--packages/integrations/svelte/client.js4
-rw-r--r--packages/integrations/vue/client.js16
10 files changed, 63 insertions, 43 deletions
diff --git a/.changeset/ninety-boats-brake.md b/.changeset/ninety-boats-brake.md
new file mode 100644
index 000000000..30c13a820
--- /dev/null
+++ b/.changeset/ninety-boats-brake.md
@@ -0,0 +1,9 @@
+---
+'@astrojs/react': patch
+'@astrojs/preact': patch
+'@astrojs/vue': patch
+'@astrojs/solid-js': patch
+'@astrojs/svelte': patch
+---
+
+Automatically unmount islands when `astro:unmount` is fired
diff --git a/.changeset/perfect-socks-hammer.md b/.changeset/perfect-socks-hammer.md
new file mode 100644
index 000000000..baae63ffe
--- /dev/null
+++ b/.changeset/perfect-socks-hammer.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Fire `astro:unmount` event when island is disconnected
diff --git a/packages/astro/components/ViewTransitions.astro b/packages/astro/components/ViewTransitions.astro
index 33741d535..15bad445d 100644
--- a/packages/astro/components/ViewTransitions.astro
+++ b/packages/astro/components/ViewTransitions.astro
@@ -163,18 +163,20 @@ const { fallback = 'animate' } = Astro.props as Props;
// Everything left in the new head is new, append it all.
document.head.append(...doc.head.children);
- // Move over persist stuff in the body
+ // Persist elements in the existing body
const oldBody = document.body;
- document.body.replaceWith(doc.body);
for (const el of oldBody.querySelectorAll(`[${PERSIST_ATTR}]`)) {
const id = el.getAttribute(PERSIST_ATTR);
- const newEl = document.querySelector(`[${PERSIST_ATTR}="${id}"]`);
+ const newEl = doc.querySelector(`[${PERSIST_ATTR}="${id}"]`);
if (newEl) {
// The element exists in the new page, replace it with the element
// from the old page so that state is preserved.
newEl.replaceWith(el);
}
}
+ // Only replace the existing body *AFTER* persistent elements are moved over
+ // This avoids disconnecting `astro-island` nodes multiple times
+ document.body.replaceWith(doc.body);
// Simulate scroll behavior of Safari and
// Chromium based browsers (Chrome, Edge, Opera, ...)
diff --git a/packages/astro/src/runtime/server/astro-island.ts b/packages/astro/src/runtime/server/astro-island.ts
index 7be630d06..e0e09eaec 100644
--- a/packages/astro/src/runtime/server/astro-island.ts
+++ b/packages/astro/src/runtime/server/astro-island.ts
@@ -51,6 +51,12 @@ declare const Astro: {
public Component: any;
public hydrator: any;
static observedAttributes = ['props'];
+ disconnectedCallback() {
+ document.addEventListener('astro:after-swap', () => {
+ // If element wasn't persisted, fire unmount event
+ if (!this.isConnected) this.dispatchEvent(new CustomEvent('astro:unmount'))
+ }, { once: true })
+ }
connectedCallback() {
if (!this.hasAttribute('await-children') || this.firstChild) {
this.childrenConnectedCallback();
diff --git a/packages/integrations/preact/src/client.ts b/packages/integrations/preact/src/client.ts
index f90614398..ad24e886b 100644
--- a/packages/integrations/preact/src/client.ts
+++ b/packages/integrations/preact/src/client.ts
@@ -1,6 +1,6 @@
-import { h, render, type JSX } from 'preact';
-import StaticHtml from './static-html.js';
import type { SignalLike } from './types';
+import { h, render, hydrate } from 'preact';
+import StaticHtml from './static-html.js';
const sharedSignalMap = new Map<string, SignalLike>();
@@ -8,7 +8,8 @@ export default (element: HTMLElement) =>
async (
Component: any,
props: Record<string, any>,
- { default: children, ...slotted }: Record<string, any>
+ { default: children, ...slotted }: Record<string, any>,
+ { client }: Record<string, string>
) => {
if (!element.hasAttribute('ssr')) return;
for (const [key, value] of Object.entries(slotted)) {
@@ -27,23 +28,13 @@ export default (element: HTMLElement) =>
}
}
- // eslint-disable-next-line @typescript-eslint/no-shadow
- function Wrapper({ children }: { children: JSX.Element }) {
- let attrs = Object.fromEntries(
- Array.from(element.attributes).map((attr) => [attr.name, attr.value])
- );
- return h(element.localName, attrs, children);
- }
-
- let parent = element.parentNode as Element;
+ const bootstrap = client !== 'only' ? hydrate : render;
- render(
- h(
- Wrapper,
- null,
- h(Component, props, children != null ? h(StaticHtml, { value: children }) : children)
- ),
- parent,
- element
+ bootstrap(
+ h(Component, props, children != null ? h(StaticHtml, { value: children }) : children),
+ element,
);
+
+ // Preact has no "unmount" option, but you can use `render(null, element)`
+ element.addEventListener('astro:unmount', () => render(null, element), { once: true })
};
diff --git a/packages/integrations/react/client-v17.js b/packages/integrations/react/client-v17.js
index 443109603..70bddc353 100644
--- a/packages/integrations/react/client-v17.js
+++ b/packages/integrations/react/client-v17.js
@@ -1,5 +1,5 @@
import { createElement } from 'react';
-import { render, hydrate } from 'react-dom';
+import { render, hydrate, unmountComponentAtNode } from 'react-dom';
import StaticHtml from './static-html.js';
export default (element) =>
@@ -12,8 +12,9 @@ export default (element) =>
props,
children != null ? createElement(StaticHtml, { value: children }) : children
);
- if (client === 'only') {
- return render(componentEl, element);
- }
- return hydrate(componentEl, element);
+
+ const isHydrate = client !== 'only';
+ const bootstrap = isHydrate ? hydrate : render;
+ bootstrap(componentEl, element);
+ element.addEventListener('astro:unmount', () => unmountComponentAtNode(element), { once: true });
};
diff --git a/packages/integrations/react/client.js b/packages/integrations/react/client.js
index d8948e7bb..dbd32c0c5 100644
--- a/packages/integrations/react/client.js
+++ b/packages/integrations/react/client.js
@@ -31,10 +31,14 @@ export default (element) =>
}
if (client === 'only') {
return startTransition(() => {
- createRoot(element).render(componentEl);
+ const root = createRoot(element);
+ root.render(componentEl);
+ element.addEventListener('astro:unmount', () => root.unmount(), { once: true });
});
}
- return startTransition(() => {
- hydrateRoot(element, componentEl, renderOptions);
+ startTransition(() => {
+ const root = hydrateRoot(element, componentEl, renderOptions);
+ root.render(componentEl);
+ element.addEventListener('astro:unmount', () => root.unmount(), { once: true });
});
};
diff --git a/packages/integrations/solid/src/client.ts b/packages/integrations/solid/src/client.ts
index 730db0f51..66b3767ea 100644
--- a/packages/integrations/solid/src/client.ts
+++ b/packages/integrations/solid/src/client.ts
@@ -9,7 +9,7 @@ export default (element: HTMLElement) =>
}
if (!element.hasAttribute('ssr')) return;
- const fn = client === 'only' ? render : hydrate;
+ const boostrap = client === 'only' ? render : hydrate;
let _slots: Record<string, any> = {};
if (Object.keys(slotted).length > 0) {
@@ -30,7 +30,7 @@ export default (element: HTMLElement) =>
const { default: children, ...slots } = _slots;
const renderId = element.dataset.solidRenderId;
- fn(
+ const dispose = boostrap(
() =>
createComponent(Component, {
...props,
@@ -42,4 +42,6 @@ export default (element: HTMLElement) =>
renderId,
}
);
+
+ element.addEventListener('astro:unmount', () => dispose(), { once: true })
};
diff --git a/packages/integrations/svelte/client.js b/packages/integrations/svelte/client.js
index 0d07ff2ba..99612a580 100644
--- a/packages/integrations/svelte/client.js
+++ b/packages/integrations/svelte/client.js
@@ -14,7 +14,7 @@ export default (target) => {
try {
if (import.meta.env.DEV) useConsoleFilter();
- new Component({
+ const component = new Component({
target,
props: {
...props,
@@ -24,6 +24,8 @@ export default (target) => {
hydrate: client !== 'only',
$$inline: true,
});
+
+ element.addEventListener('astro:unmount', () => component.$destroy(), { once: true })
} catch (e) {
} finally {
if (import.meta.env.DEV) finishUsingConsoleFilter();
diff --git a/packages/integrations/vue/client.js b/packages/integrations/vue/client.js
index ca61116b2..8b2a5eede 100644
--- a/packages/integrations/vue/client.js
+++ b/packages/integrations/vue/client.js
@@ -21,15 +21,13 @@ export default (element) =>
content = h(Suspense, null, content);
}
- if (client === 'only') {
- const app = createApp({ name, render: () => content });
- await setup(app);
- app.mount(element, false);
- } else {
- const app = createSSRApp({ name, render: () => content });
- await setup(app);
- app.mount(element, true);
- }
+ const isHydrate = client !== 'only';
+ const boostrap = isHydrate ? createSSRApp : createApp;
+ const app = boostrap({ name, render: () => content });
+ await setup(app);
+ app.mount(element, isHydrate);
+
+ element.addEventListener('astro:unmount', () => app.unmount(), { once: true });
};
function isAsync(fn) {