Skip to content

Commit 9250ac9

Browse files
authored
use options.unmount instead of overriding component.componentWillUnmount (#2919)
* use options.unmount instead of overriding component.componentWillUnmount * add test cases, allow hydrated suspended component can be unmounted
1 parent 3efb2d0 commit 9250ac9

File tree

5 files changed

+162
-13
lines changed

5 files changed

+162
-13
lines changed

compat/src/internal.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export interface Component<P = {}, S = {}> extends PreactComponent<P, S> {
1717
// Suspense internal properties
1818
_childDidSuspend?(error: Promise<void>, suspendingVNode: VNode): void;
1919
_suspended: (vnode: VNode) => (unsuspend: () => void) => void;
20-
_suspendedComponentWillUnmount?(): void;
20+
_onResolve?(): void;
2121

2222
// Portal internal properties
2323
_temp: any;

compat/src/suspense.js

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,25 @@ options._catchError = function(error, newVNode, oldVNode) {
2222
oldCatchError(error, newVNode, oldVNode);
2323
};
2424

25+
const oldUnmount = options.unmount;
26+
options.unmount = function(vnode) {
27+
/** @type {import('./internal').Component} */
28+
const component = vnode._component;
29+
if (component && component._onResolve) {
30+
component._onResolve();
31+
}
32+
33+
// if the component is still hydrating
34+
// most likely it is because the component is suspended
35+
// we set the vnode.type as `null` so that it is not a typeof function
36+
// so the unmount will remove the vnode._dom
37+
if (component && vnode._hydrating === true) {
38+
vnode.type = null;
39+
}
40+
41+
if (oldUnmount) oldUnmount(vnode);
42+
};
43+
2544
function detachedClone(vnode, detachedParent, parentDom) {
2645
if (vnode) {
2746
if (vnode._component && vnode._component.__hooks) {
@@ -109,8 +128,7 @@ Suspense.prototype._childDidSuspend = function(promise, suspendingVNode) {
109128
if (resolved) return;
110129

111130
resolved = true;
112-
suspendingComponent.componentWillUnmount =
113-
suspendingComponent._suspendedComponentWillUnmount;
131+
suspendingComponent._onResolve = null;
114132

115133
if (resolve) {
116134
resolve(onSuspensionComplete);
@@ -119,15 +137,7 @@ Suspense.prototype._childDidSuspend = function(promise, suspendingVNode) {
119137
}
120138
};
121139

122-
suspendingComponent._suspendedComponentWillUnmount =
123-
suspendingComponent.componentWillUnmount;
124-
suspendingComponent.componentWillUnmount = () => {
125-
onResolved();
126-
127-
if (suspendingComponent._suspendedComponentWillUnmount) {
128-
suspendingComponent._suspendedComponentWillUnmount();
129-
}
130-
};
140+
suspendingComponent._onResolve = onResolved;
131141

132142
const onSuspensionComplete = () => {
133143
if (!--c._pendingSuspensionCount) {

compat/src/util.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,8 @@ export function shallowDiffers(a, b) {
2121
for (let i in b) if (i !== '__source' && a[i] !== b[i]) return true;
2222
return false;
2323
}
24+
25+
export function removeNode(node) {
26+
let parentNode = node.parentNode;
27+
if (parentNode) parentNode.removeChild(node);
28+
}

compat/test/browser/suspense-hydration.test.js

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,140 @@ describe('suspense hydration', () => {
182182
});
183183
});
184184

185+
it('should allow parents to update around suspense boundary and unmount', async () => {
186+
scratch.innerHTML = '<div>Count: 0</div><div>Hello</div>';
187+
clearLog();
188+
189+
const [Lazy, resolve] = createLazy();
190+
191+
/** @type {() => void} */
192+
let increment;
193+
function Counter() {
194+
const [count, setCount] = useState(0);
195+
increment = () => setCount(c => c + 1);
196+
return (
197+
<Fragment>
198+
<div>Count: {count}</div>
199+
<Suspense>
200+
<Lazy />
201+
</Suspense>
202+
</Fragment>
203+
);
204+
}
205+
206+
let hide;
207+
function Component() {
208+
const [show, setShow] = useState(true);
209+
hide = () => setShow(false);
210+
211+
return show ? <Counter /> : null;
212+
}
213+
214+
hydrate(<Component />, scratch);
215+
rerender(); // Flush rerender queue to mimic what preact will really do
216+
expect(scratch.innerHTML).to.equal('<div>Count: 0</div><div>Hello</div>');
217+
// Re: DOM OP below - Known issue with hydrating merged text nodes
218+
expect(getLog()).to.deep.equal(['<div>Count: .appendChild(#text)']);
219+
clearLog();
220+
221+
increment();
222+
rerender();
223+
224+
expect(scratch.innerHTML).to.equal('<div>Count: 1</div><div>Hello</div>');
225+
expect(getLog()).to.deep.equal([]);
226+
clearLog();
227+
228+
await resolve(() => <div>Hello</div>);
229+
rerender();
230+
expect(scratch.innerHTML).to.equal('<div>Count: 1</div><div>Hello</div>');
231+
expect(getLog()).to.deep.equal([]);
232+
clearLog();
233+
234+
hide();
235+
rerender();
236+
expect(scratch.innerHTML).to.equal('');
237+
});
238+
239+
it('should allow parents to update around suspense boundary and unmount before resolves', async () => {
240+
scratch.innerHTML = '<div>Count: 0</div><div>Hello</div>';
241+
clearLog();
242+
243+
const [Lazy] = createLazy();
244+
245+
/** @type {() => void} */
246+
let increment;
247+
function Counter() {
248+
const [count, setCount] = useState(0);
249+
increment = () => setCount(c => c + 1);
250+
return (
251+
<Fragment>
252+
<div>Count: {count}</div>
253+
<Suspense>
254+
<Lazy />
255+
</Suspense>
256+
</Fragment>
257+
);
258+
}
259+
260+
let hide;
261+
function Component() {
262+
const [show, setShow] = useState(true);
263+
hide = () => setShow(false);
264+
265+
return show ? <Counter /> : null;
266+
}
267+
268+
hydrate(<Component />, scratch);
269+
rerender(); // Flush rerender queue to mimic what preact will really do
270+
expect(scratch.innerHTML).to.equal('<div>Count: 0</div><div>Hello</div>');
271+
// Re: DOM OP below - Known issue with hydrating merged text nodes
272+
expect(getLog()).to.deep.equal(['<div>Count: .appendChild(#text)']);
273+
clearLog();
274+
275+
increment();
276+
rerender();
277+
278+
expect(scratch.innerHTML).to.equal('<div>Count: 1</div><div>Hello</div>');
279+
expect(getLog()).to.deep.equal([]);
280+
clearLog();
281+
282+
hide();
283+
rerender();
284+
expect(scratch.innerHTML).to.equal('');
285+
});
286+
287+
it('should allow parents to unmount before resolves', async () => {
288+
scratch.innerHTML = '<div>Count: 0</div><div>Hello</div>';
289+
290+
const [Lazy] = createLazy();
291+
292+
function Counter() {
293+
return (
294+
<Fragment>
295+
<div>Count: 0</div>
296+
<Suspense>
297+
<Lazy />
298+
</Suspense>
299+
</Fragment>
300+
);
301+
}
302+
303+
let hide;
304+
function Component() {
305+
const [show, setShow] = useState(true);
306+
hide = () => setShow(false);
307+
308+
return show ? <Counter /> : null;
309+
}
310+
311+
hydrate(<Component />, scratch);
312+
rerender(); // Flush rerender queue to mimic what preact will really do
313+
314+
hide();
315+
rerender();
316+
expect(scratch.innerHTML).to.equal('');
317+
});
318+
185319
it('should properly hydrate when there is DOM and Components between Suspense and suspender', () => {
186320
scratch.innerHTML = '<div><div>Hello</div></div>';
187321
clearLog();

mangle.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"$_children": "__k",
4747
"$_pendingSuspensionCount": "__u",
4848
"$_childDidSuspend": "__c",
49-
"$_suspendedComponentWillUnmount": "__c",
49+
"$_onResolve": "__R",
5050
"$_suspended": "__e",
5151
"$_dom": "__e",
5252
"$_hydrating": "__h",

0 commit comments

Comments
 (0)