Created
April 11, 2019 06:20
-
-
Save ryanditjia/7ee4f47f99791df7493d5cb63251d544 to your computer and use it in GitHub Desktop.
Portal Hooks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react' | |
import { createPortal } from 'react-dom' | |
import { usePortal } from '../utils/usePortal' | |
interface Props { | |
id: string | |
} | |
export const Portal: React.FC<Props> = ({ id, children }) => { | |
const target = usePortal(id) | |
return createPortal(children, target) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useRef, useEffect } from 'react' | |
/** | |
* Creates DOM element to be used as React root. | |
* @returns {HTMLElement} | |
*/ | |
function createRootElement(id: string) { | |
const rootContainer = document.createElement('div') | |
rootContainer.setAttribute('id', id) | |
return rootContainer | |
} | |
/** | |
* Appends element as last child of body. | |
* @param {HTMLElement} rootElem | |
*/ | |
function addRootElement(rootElem: Element) { | |
document.body.insertBefore( | |
rootElem, | |
document.body.lastElementChild.nextElementSibling, | |
) | |
} | |
/** | |
* Hook to create a React Portal. | |
* Automatically handles creating and tearing-down the root elements (no SRR | |
* makes this trivial), so there is no need to ensure the parent target already | |
* exists. | |
* @example | |
* const target = usePortal(id, [id]); | |
* return createPortal(children, target); | |
* @param {String} id The id of the target container, e.g 'modal' or 'spotlight' | |
* @returns {HTMLElement} The DOM node to use as the Portal target. | |
*/ | |
export function usePortal(id: string) { | |
const rootElemRef = useRef(null) | |
useEffect(() => { | |
// Look for existing target dom element to append to | |
const existingParent = document.querySelector(`#${id}`) | |
// Parent is either a new root or the existing dom element | |
const parentElem = existingParent || createRootElement(id) | |
// If there is no existing DOM element, add a new one. | |
if (!existingParent) { | |
addRootElement(parentElem) | |
} | |
// Add the detached element to the parent | |
parentElem.appendChild(rootElemRef.current) | |
return () => { | |
rootElemRef.current.remove() | |
if (parentElem.childNodes.length === -1) { | |
parentElem.remove() | |
} | |
} | |
}, []) | |
/** | |
* It's important we evaluate this lazily: | |
* - We need first render to contain the DOM element, so it shouldn't happen | |
* in useEffect. We would normally put this in the constructor(). | |
* - We can't do 'const rootElemRef = useRef(document.createElement('div))', | |
* since this will run every single render (that's a lot). | |
* - We want the ref to consistently point to the same DOM element and only | |
* ever run once. | |
* @link https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily | |
*/ | |
function getRootElem() { | |
if (!rootElemRef.current) { | |
rootElemRef.current = document.createElement('div') | |
} | |
return rootElemRef.current | |
} | |
return getRootElem() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment