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 = '
Hello
'; 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",