Skip to content

Commit 748e1a8

Browse files
committed
Handle errors in componentWillUnmount and clean up appropriately.
1 parent db19587 commit 748e1a8

File tree

3 files changed

+90
-15
lines changed

3 files changed

+90
-15
lines changed

src/component.ts

+25-9
Original file line numberDiff line numberDiff line change
@@ -207,22 +207,38 @@ abstract class Component<P, S> {
207207
allComponentsMap: Record<string, Component<any, any> | undefined> | null,
208208
): Promise<void> {
209209
return this._lock(async () => {
210-
const promises = Object.keys(this._childMap).map(key => {
211-
const childPromises = this._childMap[key]!.map(child => {
210+
const promises = Object.keys(this._childMap).flatMap(key => {
211+
return this._childMap[key]!.map(async child => {
212212
if (child instanceof Component) {
213213
return child._triggerUnmount(allComponentsMap)
214214
}
215-
return
216215
})
217-
return Promise.all(childPromises)
218216
})
219-
await Promise.all(promises)
220217

221-
this.componentWillUnmount()
222-
if (allComponentsMap) {
223-
delete allComponentsMap[this._id]
218+
try {
219+
await Promise.all(promises)
220+
} catch (error) {
221+
// If a child errors while unmounting, still wait for all children to be
222+
// processed before calling the parent's componentWillUnmount.
223+
await Promise.allSettled(promises)
224+
225+
if (process.env.NODE_ENV !== "test") {
226+
// Don't suppress the error, but ensure the finally block unmounts and
227+
// cleans up appropriately.
228+
throw error
229+
}
230+
} finally {
231+
// The user-defined componentWillUnmount() may error, but we still need
232+
// to clean up.
233+
try {
234+
this.componentWillUnmount()
235+
} finally {
236+
if (allComponentsMap) {
237+
delete allComponentsMap[this._id]
238+
}
239+
this._unmounted = true
240+
}
224241
}
225-
this._unmounted = true
226242
})
227243
}
228244

test/purview_test.tsx

+64-5
Original file line numberDiff line numberDiff line change
@@ -1390,10 +1390,65 @@ test("locked mount cycle", async () => {
13901390
})
13911391

13921392
test("componentWillUnmount", async () => {
1393-
let unmounted = false
1394-
class Foo extends TestComponent<{}, { text: string }> {
1393+
let unmountCalled = false
1394+
let instance: Foo = null as any
1395+
class Foo extends TestComponent<{}, {}> {
1396+
constructor(props: {}) {
1397+
super(props)
1398+
instance = this
1399+
}
1400+
1401+
componentWillUnmount(): void {
1402+
unmountCalled = true
1403+
}
1404+
1405+
render(): JSX.Element {
1406+
return <div />
1407+
}
1408+
}
1409+
1410+
await renderAndConnect(<Foo />, async () => {
1411+
expect(unmountCalled).toBe(false)
1412+
expect(instance._unmounted).toBe(false)
1413+
})
1414+
1415+
// Must wait for close to propagate to server.
1416+
await new Promise(resolve => setTimeout(resolve, 25))
1417+
expect(unmountCalled).toBe(true)
1418+
expect(instance._unmounted).toBe(true)
1419+
})
1420+
1421+
test("componentWillUnmount nested error", async () => {
1422+
let unmountCalled = false
1423+
let fooInstance: Foo = null as any
1424+
class Foo extends TestComponent<{}, {}> {
1425+
constructor(props: {}) {
1426+
super(props)
1427+
fooInstance = this
1428+
}
1429+
1430+
componentWillUnmount(): void {
1431+
unmountCalled = true
1432+
}
1433+
1434+
render(): JSX.Element {
1435+
return <Bar />
1436+
}
1437+
}
1438+
1439+
let barInstance: Bar = null as any
1440+
class Bar extends TestComponent<{}, {}> {
1441+
static getUniqueName(): string {
1442+
return `${super.getUniqueName()}-bar`
1443+
}
1444+
1445+
constructor(props: {}) {
1446+
super(props)
1447+
barInstance = this
1448+
}
1449+
13951450
componentWillUnmount(): void {
1396-
unmounted = true
1451+
throw new Error("Bar unmount error")
13971452
}
13981453

13991454
render(): JSX.Element {
@@ -1402,12 +1457,16 @@ test("componentWillUnmount", async () => {
14021457
}
14031458

14041459
await renderAndConnect(<Foo />, async () => {
1405-
expect(unmounted).toBe(false)
1460+
expect(unmountCalled).toBe(false)
1461+
expect(barInstance._unmounted).toBe(false)
1462+
expect(fooInstance._unmounted).toBe(false)
14061463
})
14071464

14081465
// Must wait for close to propagate to server.
14091466
await new Promise(resolve => setTimeout(resolve, 25))
1410-
expect(unmounted).toBe(true)
1467+
expect(unmountCalled).toBe(true)
1468+
expect(barInstance._unmounted).toBe(true)
1469+
expect(fooInstance._unmounted).toBe(true)
14111470
})
14121471

14131472
test("componentWillReceiveProps", async () => {

tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"compilerOptions": {
3-
"lib": ["es2017", "dom"],
3+
"lib": ["es2020", "dom"],
44
"target": "es2017",
55
"module": "commonjs",
66
"moduleResolution": "node",

0 commit comments

Comments
 (0)