diff --git a/compat/src/internal.d.ts b/compat/src/internal.d.ts
index 6807139d07..cb68ffa89d 100644
--- a/compat/src/internal.d.ts
+++ b/compat/src/internal.d.ts
@@ -17,7 +17,7 @@ export interface Component
extends PreactComponent
{
// Suspense internal properties
_childDidSuspend?(error: Promise, suspendingVNode: VNode): void;
_suspended: (vnode: VNode) => (unsuspend: () => void) => void;
- _suspendedComponentWillUnmount?(): void;
+ _onResolve?(): void;
// Portal internal properties
_temp: any;
diff --git a/compat/src/suspense.js b/compat/src/suspense.js
index 5301b63ae9..6244eafbd2 100644
--- a/compat/src/suspense.js
+++ b/compat/src/suspense.js
@@ -22,6 +22,25 @@ options._catchError = function(error, newVNode, oldVNode) {
oldCatchError(error, newVNode, oldVNode);
};
+const oldUnmount = options.unmount;
+options.unmount = function(vnode) {
+ /** @type {import('./internal').Component} */
+ const component = vnode._component;
+ if (component && component._onResolve) {
+ component._onResolve();
+ }
+
+ // if the component is still hydrating
+ // most likely it is because the component is suspended
+ // we set the vnode.type as `null` so that it is not a typeof function
+ // so the unmount will remove the vnode._dom
+ if (component && vnode._hydrating === true) {
+ vnode.type = null;
+ }
+
+ if (oldUnmount) oldUnmount(vnode);
+};
+
function detachedClone(vnode, detachedParent, parentDom) {
if (vnode) {
if (vnode._component && vnode._component.__hooks) {
@@ -109,8 +128,7 @@ Suspense.prototype._childDidSuspend = function(promise, suspendingVNode) {
if (resolved) return;
resolved = true;
- suspendingComponent.componentWillUnmount =
- suspendingComponent._suspendedComponentWillUnmount;
+ suspendingComponent._onResolve = null;
if (resolve) {
resolve(onSuspensionComplete);
@@ -119,15 +137,7 @@ Suspense.prototype._childDidSuspend = function(promise, suspendingVNode) {
}
};
- suspendingComponent._suspendedComponentWillUnmount =
- suspendingComponent.componentWillUnmount;
- suspendingComponent.componentWillUnmount = () => {
- onResolved();
-
- if (suspendingComponent._suspendedComponentWillUnmount) {
- suspendingComponent._suspendedComponentWillUnmount();
- }
- };
+ suspendingComponent._onResolve = onResolved;
const onSuspensionComplete = () => {
if (!--c._pendingSuspensionCount) {
diff --git a/compat/src/util.js b/compat/src/util.js
index e7e5630e00..fa12b09876 100644
--- a/compat/src/util.js
+++ b/compat/src/util.js
@@ -21,3 +21,8 @@ export function shallowDiffers(a, b) {
for (let i in b) if (i !== '__source' && a[i] !== b[i]) return true;
return false;
}
+
+export function removeNode(node) {
+ let parentNode = node.parentNode;
+ if (parentNode) parentNode.removeChild(node);
+}
diff --git a/compat/test/browser/suspense-hydration.test.js b/compat/test/browser/suspense-hydration.test.js
index 0671f319a2..b8d45f89f4 100644
--- a/compat/test/browser/suspense-hydration.test.js
+++ b/compat/test/browser/suspense-hydration.test.js
@@ -182,6 +182,140 @@ describe('suspense hydration', () => {
});
});
+ it('should allow parents to update around suspense boundary and unmount', async () => {
+ scratch.innerHTML = 'Count: 0
Hello
';
+ clearLog();
+
+ const [Lazy, resolve] = createLazy();
+
+ /** @type {() => void} */
+ let increment;
+ function Counter() {
+ const [count, setCount] = useState(0);
+ increment = () => setCount(c => c + 1);
+ return (
+
+ Count: {count}
+
+
+
+
+ );
+ }
+
+ let hide;
+ function Component() {
+ const [show, setShow] = useState(true);
+ hide = () => setShow(false);
+
+ return show ? : null;
+ }
+
+ hydrate(, scratch);
+ rerender(); // Flush rerender queue to mimic what preact will really do
+ expect(scratch.innerHTML).to.equal('Count: 0
Hello
');
+ // Re: DOM OP below - Known issue with hydrating merged text nodes
+ expect(getLog()).to.deep.equal(['Count: .appendChild(#text)']);
+ clearLog();
+
+ increment();
+ rerender();
+
+ expect(scratch.innerHTML).to.equal('
Count: 1
Hello
');
+ expect(getLog()).to.deep.equal([]);
+ clearLog();
+
+ await resolve(() =>
Hello
);
+ rerender();
+ expect(scratch.innerHTML).to.equal('
Count: 1
Hello
');
+ expect(getLog()).to.deep.equal([]);
+ clearLog();
+
+ hide();
+ rerender();
+ expect(scratch.innerHTML).to.equal('');
+ });
+
+ it('should allow parents to update around suspense boundary and unmount before resolves', async () => {
+ scratch.innerHTML = '
Count: 0
Hello
';
+ clearLog();
+
+ const [Lazy] = createLazy();
+
+ /** @type {() => void} */
+ let increment;
+ function Counter() {
+ const [count, setCount] = useState(0);
+ increment = () => setCount(c => c + 1);
+ return (
+
+ Count: {count}
+
+
+
+
+ );
+ }
+
+ let hide;
+ function Component() {
+ const [show, setShow] = useState(true);
+ hide = () => setShow(false);
+
+ return show ?
: null;
+ }
+
+ hydrate(
, scratch);
+ rerender(); // Flush rerender queue to mimic what preact will really do
+ expect(scratch.innerHTML).to.equal('
Count: 0
Hello
');
+ // Re: DOM OP below - Known issue with hydrating merged text nodes
+ expect(getLog()).to.deep.equal(['
Count: .appendChild(#text)']);
+ clearLog();
+
+ increment();
+ rerender();
+
+ expect(scratch.innerHTML).to.equal('
Count: 1
Hello
');
+ expect(getLog()).to.deep.equal([]);
+ clearLog();
+
+ hide();
+ rerender();
+ expect(scratch.innerHTML).to.equal('');
+ });
+
+ it('should allow parents to unmount before resolves', async () => {
+ scratch.innerHTML = '
Count: 0
Hello
';
+
+ const [Lazy] = createLazy();
+
+ function Counter() {
+ return (
+
+ Count: 0
+
+
+
+
+ );
+ }
+
+ let hide;
+ function Component() {
+ const [show, setShow] = useState(true);
+ hide = () => setShow(false);
+
+ return show ?
: null;
+ }
+
+ hydrate(
, scratch);
+ rerender(); // Flush rerender queue to mimic what preact will really do
+
+ hide();
+ rerender();
+ expect(scratch.innerHTML).to.equal('');
+ });
+
it('should properly hydrate when there is DOM and Components between Suspense and suspender', () => {
scratch.innerHTML = '
';
clearLog();
diff --git a/mangle.json b/mangle.json
index 0f850a3f0a..8a1f49030b 100644
--- a/mangle.json
+++ b/mangle.json
@@ -46,7 +46,7 @@
"$_children": "__k",
"$_pendingSuspensionCount": "__u",
"$_childDidSuspend": "__c",
- "$_suspendedComponentWillUnmount": "__c",
+ "$_onResolve": "__R",
"$_suspended": "__e",
"$_dom": "__e",
"$_hydrating": "__h",