Skip to content

Commit 9d0a07f

Browse files
author
Kent C. Dodds
committed
feat(unmount): add unmount to render object
1 parent 03b17c3 commit 9d0a07f

File tree

3 files changed

+78
-1
lines changed

3 files changed

+78
-1
lines changed

README.md

+18-1
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,21 @@ The containing DOM node of your rendered React Element (rendered using
127127

128128
> Tip: To get the root element of your rendered element, use `container.firstChild`.
129129
130+
#### `unmount`
131+
132+
This will cause the rendered component to be unmounted. This is useful for
133+
testing what happens when your component is removed from the page (like testing
134+
that you don't leave event handlers hanging around causing memory leaks).
135+
136+
> This method is a pretty small abstraction over
137+
> `ReactDOM.unmountComponentAtNode`
138+
139+
```javascript
140+
const {container, unmount} = render(<Login />)
141+
unmount()
142+
// your component has been unmounted and now: container.innerHTML === ''
143+
```
144+
130145
#### `queryByTestId`
131146

132147
A shortcut to `` container.querySelector(`[data-testid="${yourId}"]`) ``. Read
@@ -270,7 +285,9 @@ Or you could include the index or an ID in your attribute:
270285
And then you could use the `queryByTestId`:
271286

272287
```javascript
273-
const items = [/* your items */]
288+
const items = [
289+
/* your items */
290+
]
274291
const {queryByTestId} = render(/* your component with the items */)
275292
const thirdItem = queryByTestId(`item-${items[2].id}`)
276293
```

src/__tests__/stopwatch.js

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React from 'react'
2+
import {render, Simulate} from '../'
3+
4+
class StopWatch extends React.Component {
5+
state = {lapse: 0, running: false}
6+
handleRunClick = () => {
7+
this.setState(state => {
8+
if (state.running) {
9+
clearInterval(this.timer)
10+
} else {
11+
const startTime = Date.now() - this.state.lapse
12+
this.timer = setInterval(() => {
13+
this.setState({lapse: Date.now() - startTime})
14+
})
15+
}
16+
return {running: !state.running}
17+
})
18+
}
19+
handleClearClick = () => {
20+
clearInterval(this.timer)
21+
this.setState({lapse: 0, running: false})
22+
}
23+
componentWillUnmount() {
24+
clearInterval(this.timer)
25+
}
26+
render() {
27+
const {lapse, running} = this.state
28+
return (
29+
<div>
30+
<span>{lapse}ms</span>
31+
<button onClick={this.handleRunClick} data-testid="start-stop-button">
32+
{running ? 'Stop' : 'Start'}
33+
</button>
34+
<button onClick={this.handleClearClick} data-testid="clear-button">
35+
Clear
36+
</button>
37+
</div>
38+
)
39+
}
40+
}
41+
42+
const wait = time => new Promise(resolve => setTimeout(resolve, time))
43+
44+
test('unmounts a component', async () => {
45+
jest.spyOn(console, 'error').mockImplementation(() => {})
46+
const {unmount, queryByTestId, container} = render(<StopWatch />)
47+
Simulate.click(queryByTestId('start-stop-button'))
48+
unmount()
49+
// hey there reader! You don't need to have an assertion like this one
50+
// this is just me making sure that the unmount function works.
51+
// You don't need to do this in your apps. Just rely on the fact that this works.
52+
expect(container.innerHTML).toBe('')
53+
// just wait to see if the interval is cleared or not
54+
// if it's not, then we'll call setState on an unmounted component
55+
// and get an error.
56+
await wait()
57+
// eslint-disable-next-line no-console
58+
expect(console.error).not.toHaveBeenCalled()
59+
})

src/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ function render(ui, {container = document.createElement('div')} = {}) {
1515
ReactDOM.render(ui, container)
1616
return {
1717
container,
18+
unmount: () => ReactDOM.unmountComponentAtNode(container),
1819
queryByTestId: queryDivByTestId.bind(null, container),
1920
}
2021
}

0 commit comments

Comments
 (0)