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
This commit is contained in:
parent
9e021a91c5
commit
1f58a7a1be
10 changed files with 63 additions and 43 deletions
9
.changeset/ninety-boats-brake.md
Normal file
9
.changeset/ninety-boats-brake.md
Normal file
|
@ -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
|
5
.changeset/perfect-socks-hammer.md
Normal file
5
.changeset/perfect-socks-hammer.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fire `astro:unmount` event when island is disconnected
|
|
@ -163,18 +163,20 @@ const { fallback = 'animate' } = Astro.props as Props;
|
||||||
// Everything left in the new head is new, append it all.
|
// Everything left in the new head is new, append it all.
|
||||||
document.head.append(...doc.head.children);
|
document.head.append(...doc.head.children);
|
||||||
|
|
||||||
// Move over persist stuff in the body
|
// Persist elements in the existing body
|
||||||
const oldBody = document.body;
|
const oldBody = document.body;
|
||||||
document.body.replaceWith(doc.body);
|
|
||||||
for (const el of oldBody.querySelectorAll(`[${PERSIST_ATTR}]`)) {
|
for (const el of oldBody.querySelectorAll(`[${PERSIST_ATTR}]`)) {
|
||||||
const id = el.getAttribute(PERSIST_ATTR);
|
const id = el.getAttribute(PERSIST_ATTR);
|
||||||
const newEl = document.querySelector(`[${PERSIST_ATTR}="${id}"]`);
|
const newEl = doc.querySelector(`[${PERSIST_ATTR}="${id}"]`);
|
||||||
if (newEl) {
|
if (newEl) {
|
||||||
// The element exists in the new page, replace it with the element
|
// The element exists in the new page, replace it with the element
|
||||||
// from the old page so that state is preserved.
|
// from the old page so that state is preserved.
|
||||||
newEl.replaceWith(el);
|
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
|
// Simulate scroll behavior of Safari and
|
||||||
// Chromium based browsers (Chrome, Edge, Opera, ...)
|
// Chromium based browsers (Chrome, Edge, Opera, ...)
|
||||||
|
|
|
@ -51,6 +51,12 @@ declare const Astro: {
|
||||||
public Component: any;
|
public Component: any;
|
||||||
public hydrator: any;
|
public hydrator: any;
|
||||||
static observedAttributes = ['props'];
|
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() {
|
connectedCallback() {
|
||||||
if (!this.hasAttribute('await-children') || this.firstChild) {
|
if (!this.hasAttribute('await-children') || this.firstChild) {
|
||||||
this.childrenConnectedCallback();
|
this.childrenConnectedCallback();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { h, render, type JSX } from 'preact';
|
|
||||||
import StaticHtml from './static-html.js';
|
|
||||||
import type { SignalLike } from './types';
|
import type { SignalLike } from './types';
|
||||||
|
import { h, render, hydrate } from 'preact';
|
||||||
|
import StaticHtml from './static-html.js';
|
||||||
|
|
||||||
const sharedSignalMap = new Map<string, SignalLike>();
|
const sharedSignalMap = new Map<string, SignalLike>();
|
||||||
|
|
||||||
|
@ -8,7 +8,8 @@ export default (element: HTMLElement) =>
|
||||||
async (
|
async (
|
||||||
Component: any,
|
Component: any,
|
||||||
props: Record<string, 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;
|
if (!element.hasAttribute('ssr')) return;
|
||||||
for (const [key, value] of Object.entries(slotted)) {
|
for (const [key, value] of Object.entries(slotted)) {
|
||||||
|
@ -27,23 +28,13 @@ export default (element: HTMLElement) =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
const bootstrap = client !== 'only' ? hydrate : render;
|
||||||
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;
|
bootstrap(
|
||||||
|
h(Component, props, children != null ? h(StaticHtml, { value: children }) : children),
|
||||||
render(
|
element,
|
||||||
h(
|
|
||||||
Wrapper,
|
|
||||||
null,
|
|
||||||
h(Component, props, children != null ? h(StaticHtml, { value: children }) : children)
|
|
||||||
),
|
|
||||||
parent,
|
|
||||||
element
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Preact has no "unmount" option, but you can use `render(null, element)`
|
||||||
|
element.addEventListener('astro:unmount', () => render(null, element), { once: true })
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { createElement } from 'react';
|
import { createElement } from 'react';
|
||||||
import { render, hydrate } from 'react-dom';
|
import { render, hydrate, unmountComponentAtNode } from 'react-dom';
|
||||||
import StaticHtml from './static-html.js';
|
import StaticHtml from './static-html.js';
|
||||||
|
|
||||||
export default (element) =>
|
export default (element) =>
|
||||||
|
@ -12,8 +12,9 @@ export default (element) =>
|
||||||
props,
|
props,
|
||||||
children != null ? createElement(StaticHtml, { value: children }) : children
|
children != null ? createElement(StaticHtml, { value: children }) : children
|
||||||
);
|
);
|
||||||
if (client === 'only') {
|
|
||||||
return render(componentEl, element);
|
const isHydrate = client !== 'only';
|
||||||
}
|
const bootstrap = isHydrate ? hydrate : render;
|
||||||
return hydrate(componentEl, element);
|
bootstrap(componentEl, element);
|
||||||
|
element.addEventListener('astro:unmount', () => unmountComponentAtNode(element), { once: true });
|
||||||
};
|
};
|
||||||
|
|
|
@ -31,10 +31,14 @@ export default (element) =>
|
||||||
}
|
}
|
||||||
if (client === 'only') {
|
if (client === 'only') {
|
||||||
return startTransition(() => {
|
return startTransition(() => {
|
||||||
createRoot(element).render(componentEl);
|
const root = createRoot(element);
|
||||||
|
root.render(componentEl);
|
||||||
|
element.addEventListener('astro:unmount', () => root.unmount(), { once: true });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return startTransition(() => {
|
startTransition(() => {
|
||||||
hydrateRoot(element, componentEl, renderOptions);
|
const root = hydrateRoot(element, componentEl, renderOptions);
|
||||||
|
root.render(componentEl);
|
||||||
|
element.addEventListener('astro:unmount', () => root.unmount(), { once: true });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,7 +9,7 @@ export default (element: HTMLElement) =>
|
||||||
}
|
}
|
||||||
if (!element.hasAttribute('ssr')) return;
|
if (!element.hasAttribute('ssr')) return;
|
||||||
|
|
||||||
const fn = client === 'only' ? render : hydrate;
|
const boostrap = client === 'only' ? render : hydrate;
|
||||||
|
|
||||||
let _slots: Record<string, any> = {};
|
let _slots: Record<string, any> = {};
|
||||||
if (Object.keys(slotted).length > 0) {
|
if (Object.keys(slotted).length > 0) {
|
||||||
|
@ -30,7 +30,7 @@ export default (element: HTMLElement) =>
|
||||||
const { default: children, ...slots } = _slots;
|
const { default: children, ...slots } = _slots;
|
||||||
const renderId = element.dataset.solidRenderId;
|
const renderId = element.dataset.solidRenderId;
|
||||||
|
|
||||||
fn(
|
const dispose = boostrap(
|
||||||
() =>
|
() =>
|
||||||
createComponent(Component, {
|
createComponent(Component, {
|
||||||
...props,
|
...props,
|
||||||
|
@ -42,4 +42,6 @@ export default (element: HTMLElement) =>
|
||||||
renderId,
|
renderId,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
element.addEventListener('astro:unmount', () => dispose(), { once: true })
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default (target) => {
|
||||||
try {
|
try {
|
||||||
if (import.meta.env.DEV) useConsoleFilter();
|
if (import.meta.env.DEV) useConsoleFilter();
|
||||||
|
|
||||||
new Component({
|
const component = new Component({
|
||||||
target,
|
target,
|
||||||
props: {
|
props: {
|
||||||
...props,
|
...props,
|
||||||
|
@ -24,6 +24,8 @@ export default (target) => {
|
||||||
hydrate: client !== 'only',
|
hydrate: client !== 'only',
|
||||||
$$inline: true,
|
$$inline: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
element.addEventListener('astro:unmount', () => component.$destroy(), { once: true })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
} finally {
|
} finally {
|
||||||
if (import.meta.env.DEV) finishUsingConsoleFilter();
|
if (import.meta.env.DEV) finishUsingConsoleFilter();
|
||||||
|
|
|
@ -21,15 +21,13 @@ export default (element) =>
|
||||||
content = h(Suspense, null, content);
|
content = h(Suspense, null, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client === 'only') {
|
const isHydrate = client !== 'only';
|
||||||
const app = createApp({ name, render: () => content });
|
const boostrap = isHydrate ? createSSRApp : createApp;
|
||||||
await setup(app);
|
const app = boostrap({ name, render: () => content });
|
||||||
app.mount(element, false);
|
await setup(app);
|
||||||
} else {
|
app.mount(element, isHydrate);
|
||||||
const app = createSSRApp({ name, render: () => content });
|
|
||||||
await setup(app);
|
element.addEventListener('astro:unmount', () => app.unmount(), { once: true });
|
||||||
app.mount(element, true);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function isAsync(fn) {
|
function isAsync(fn) {
|
||||||
|
|
Loading…
Reference in a new issue