Skip to content

Commit 162b332

Browse files
Convert Portal to functional component, properly remove DOM element on unmount (#1549)
Resolves #1365
1 parent 59b9669 commit 162b332

File tree

2 files changed

+59
-26
lines changed

2 files changed

+59
-26
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React from 'react'
2+
import { render, screen } from '@testing-library/react'
3+
import Portal from '../src/Portal'
4+
5+
describe('Portal', () => {
6+
it('should append container to document body', () => {
7+
render(<div>children</div>)
8+
render(<Portal>children</Portal>)
9+
10+
const elements = screen.getAllByText('children')
11+
12+
expect(elements[1]).toHaveAttribute('evergreen-portal-container')
13+
})
14+
15+
it('should render children', () => {
16+
render(
17+
<Portal>
18+
<span data-testid="children">Hello world</span>
19+
</Portal>
20+
)
21+
22+
const children = screen.getByTestId('children')
23+
24+
expect(children).toHaveTextContent('Hello world')
25+
expect(children.parentNode).toHaveAttribute('evergreen-portal-container')
26+
})
27+
28+
it('should remove DOM element when unmounted', () => {
29+
const { unmount } = render(<Portal>children</Portal>)
30+
31+
unmount()
32+
33+
expect(screen.queryByText('children')).toBeNull()
34+
expect(document.querySelector('[evergreen-portal-container]')).toBeNull()
35+
})
36+
})

src/portal/src/Portal.js

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,37 @@
1-
import { Component } from 'react'
1+
import { useRef, useState } from 'react'
22
import PropTypes from 'prop-types'
3-
import ReactDOM from 'react-dom'
4-
import canUseDom from '../../lib/canUseDom'
3+
import { createPortal } from 'react-dom'
4+
import { useIsomorphicLayoutEffect } from '../../hooks'
55

6-
let portalContainer
6+
// Based on https://github.com/mantinedev/mantine/blob/master/src/mantine-core/src/Portal/Portal.tsx
7+
const Portal = props => {
8+
const { children } = props
79

8-
export default class Portal extends Component {
9-
constructor() {
10-
super()
10+
const [mounted, setMounted] = useState(false)
11+
const ref = useRef()
1112

12-
// This fixes SSR
13-
if (!canUseDom) return
13+
useIsomorphicLayoutEffect(() => {
14+
setMounted(true)
1415

15-
if (!portalContainer) {
16-
portalContainer = document.createElement('div')
17-
portalContainer.setAttribute('evergreen-portal-container', '')
18-
document.body.appendChild(portalContainer)
19-
}
20-
}
16+
ref.current = document.createElement('div')
17+
ref.current.setAttribute('evergreen-portal-container', '')
2118

22-
componentDidMount() {
23-
this.el = document.createElement('div')
24-
portalContainer.appendChild(this.el)
25-
this.forceUpdate()
26-
}
19+
document.body.appendChild(ref.current)
2720

28-
componentWillUnmount() {
29-
portalContainer.removeChild(this.el)
30-
}
21+
return () => {
22+
document.body.removeChild(ref.current)
23+
}
24+
}, [])
3125

32-
render() {
33-
if (!this.el) return null
34-
return ReactDOM.createPortal(this.props.children, this.el)
26+
if (!mounted) {
27+
return null
3528
}
29+
30+
return createPortal(children, ref.current)
3631
}
3732

3833
Portal.propTypes = {
3934
children: PropTypes.node.isRequired
4035
}
36+
37+
export default Portal

0 commit comments

Comments
 (0)