Skip to content

Commit 8d0fc82

Browse files
committed
Refactor addCleanup to not treat unmount differently for external cleanups
1 parent 01918d9 commit 8d0fc82

File tree

5 files changed

+128
-58
lines changed

5 files changed

+128
-58
lines changed

docs/api-reference.md

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,9 @@ This is the same [`act` function](https://reactjs.org/docs/test-utils.html#act)
109109
function cleanup: Promise<void>
110110
```
111111

112-
Unmounts any rendered hooks rendered with `renderHook`, ensuring all effects have been flushed.
112+
Unmounts any rendered hooks rendered with `renderHook`, ensuring all effects have been flushed. Any
113+
callbacks added with [`addCleanup`](<(/reference/api#addCleanup).>) will also be called when
114+
`cleanup` is run.
113115

114116
> Please note that this is done automatically if the testing framework you're using supports the
115117
> `afterEach` global (like Jest, mocha and Jasmine). If not, you will need to do manual cleanups
@@ -151,26 +153,30 @@ variable to `true` before importing `@testing-library/react-hooks` will also dis
151153
## `addCleanup`
152154
153155
```js
154-
function addCleanup(
155-
callback: function(props?: any): any
156-
): void
156+
function addCleanup(callback: function(): void|Promise<void>): function(): void
157157
```
158158

159-
Callback to be called after `cleanup`.
159+
Add a callback to be called during [`cleanup`](/reference/api#cleanup), returning a function to
160+
remove the cleanup if is no longer required. Cleanups are called in reverse order to being added.
161+
This is usually only relevant when wanting a cleanup to run after the component has been unmounted.
160162

161-
In some cases you might want to run some callback after internal `cleanup` happen, especially after
162-
`unmount` happens in `cleanup`. If the sequence matters to you, you could use `addCleanup`.
163+
If the provided callback is an `async` function or returns a promise, `cleanup` will wait for it to
164+
be resolved before moving onto the next cleanup callback.
163165

164-
```js
165-
import { addCleanup } from '@testing-library/react-hooks'
166+
> Please note that any cleanups added using `addCleanup` are removed after `cleanup` is called. For
167+
> cleanups that need to run with every test, it is advised to add them in a `beforeEach` block (or
168+
> equivalent for your test runner).
166169

167-
jest.useFakeTimers()
170+
## `removeCleanup`
168171

169-
addCleanup(() => {
170-
jest.runOnlyPendingTimers()
171-
})
172+
```js
173+
function removeCleanup(callback: function(): void|Promise<void>): void
172174
```
173175

176+
Removes a cleanup callback previously added with [`addCleanup`](/reference/api#addCleanup). Once
177+
removed, the provided callback will no longer execute as part of running
178+
[`cleanup`](/reference/api#cleanup).
179+
174180
---
175181

176182
## Async Utilities

src/cleanup.js

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,22 @@
11
import flushMicroTasks from './flush-microtasks'
22

3-
let internalCleanupCbs = []
4-
let cleanupCbs = []
3+
let cleanupCallbacks = []
54

65
async function cleanup() {
76
await flushMicroTasks()
8-
internalCleanupCbs.forEach((cb) => cb())
9-
internalCleanupCbs = []
10-
cleanupCbs.forEach((cb) => cb())
11-
}
12-
13-
function addInternalCleanup(callback) {
14-
internalCleanupCbs.push(callback)
7+
for (const callback of cleanupCallbacks) {
8+
await callback()
9+
}
10+
cleanupCallbacks = []
1511
}
1612

1713
function addCleanup(callback) {
18-
cleanupCbs.push(callback)
14+
cleanupCallbacks.unshift(callback)
15+
return () => removeCleanup(callback)
1916
}
2017

21-
function removeInternalCleanup(callback) {
22-
internalCleanupCbs = internalCleanupCbs.filter((cb) => cb !== callback)
18+
function removeCleanup(callback) {
19+
cleanupCallbacks = cleanupCallbacks.filter((cb) => cb !== callback)
2320
}
2421

25-
export { cleanup, addCleanup, addInternalCleanup, removeInternalCleanup }
22+
export { cleanup, addCleanup, removeCleanup }

src/pure.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { Suspense } from 'react'
22
import { act, create } from 'react-test-renderer'
33
import asyncUtils from './asyncUtils'
4-
import { cleanup, addCleanup, addInternalCleanup, removeInternalCleanup } from './cleanup'
4+
import { cleanup, addCleanup, removeCleanup } from './cleanup'
55

66
function TestHook({ callback, hookProps, onError, children }) {
77
try {
@@ -84,12 +84,12 @@ function renderHook(callback, { initialProps, wrapper } = {}) {
8484

8585
function unmountHook() {
8686
act(() => {
87-
removeInternalCleanup(unmountHook)
87+
removeCleanup(unmountHook)
8888
unmount()
8989
})
9090
}
9191

92-
addInternalCleanup(unmountHook)
92+
addCleanup(unmountHook)
9393

9494
return {
9595
result,
@@ -99,4 +99,4 @@ function renderHook(callback, { initialProps, wrapper } = {}) {
9999
}
100100
}
101101

102-
export { renderHook, cleanup, act, addCleanup }
102+
export { renderHook, cleanup, addCleanup, removeCleanup, act }

test/addCleanup.test.js

Lines changed: 0 additions & 27 deletions
This file was deleted.

test/cleanup.test.js

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useEffect } from 'react'
2-
import { renderHook, cleanup } from 'src'
2+
import { renderHook, cleanup, addCleanup, removeCleanup } from 'src/pure'
33

44
describe('cleanup tests', () => {
55
test('should flush effects on cleanup', async () => {
@@ -38,4 +38,98 @@ describe('cleanup tests', () => {
3838
expect(cleanupCalled[1]).toBe(true)
3939
expect(cleanupCalled[2]).toBe(true)
4040
})
41+
42+
test('should call cleanups in reverse order', async () => {
43+
let callSequence = []
44+
addCleanup(() => {
45+
callSequence.push('cleanup')
46+
})
47+
addCleanup(() => {
48+
callSequence.push('another cleanup')
49+
})
50+
const hookWithCleanup = () => {
51+
useEffect(() => {
52+
return () => {
53+
callSequence.push('unmount')
54+
}
55+
})
56+
}
57+
renderHook(() => hookWithCleanup())
58+
59+
await cleanup()
60+
61+
expect(callSequence).toEqual(['unmount', 'another cleanup', 'cleanup'])
62+
})
63+
64+
test('should wait for async cleanup', async () => {
65+
let callSequence = []
66+
addCleanup(() => {
67+
callSequence.push('cleanup')
68+
})
69+
addCleanup(async () => {
70+
await new Promise((resolve) => setTimeout(resolve, 10))
71+
callSequence.push('another cleanup')
72+
})
73+
const hookWithCleanup = () => {
74+
useEffect(() => {
75+
return () => {
76+
callSequence.push('unmount')
77+
}
78+
})
79+
}
80+
renderHook(() => hookWithCleanup())
81+
82+
await cleanup()
83+
84+
expect(callSequence).toEqual(['unmount', 'another cleanup', 'cleanup'])
85+
})
86+
87+
test('should remove cleanup using removeCleanup', async () => {
88+
let callSequence = []
89+
addCleanup(() => {
90+
callSequence.push('cleanup')
91+
})
92+
const anotherCleanup = () => {
93+
callSequence.push('another cleanup')
94+
}
95+
addCleanup(anotherCleanup)
96+
const hookWithCleanup = () => {
97+
useEffect(() => {
98+
return () => {
99+
callSequence.push('unmount')
100+
}
101+
})
102+
}
103+
renderHook(() => hookWithCleanup())
104+
105+
removeCleanup(anotherCleanup)
106+
107+
await cleanup()
108+
109+
expect(callSequence).toEqual(['unmount', 'cleanup'])
110+
})
111+
112+
test('should remove cleanup using returned handler', async () => {
113+
let callSequence = []
114+
addCleanup(() => {
115+
callSequence.push('cleanup')
116+
})
117+
const remove = addCleanup(() => {
118+
callSequence.push('another cleanup')
119+
})
120+
const hookWithCleanup = () => {
121+
useEffect(() => {
122+
return () => {
123+
callSequence.push('unmount')
124+
}
125+
})
126+
}
127+
renderHook(() => hookWithCleanup())
128+
129+
remove()
130+
131+
await cleanup()
132+
133+
expect(callSequence).toEqual(['unmount', 'cleanup'])
134+
})
41135
})

0 commit comments

Comments
 (0)