Last active
July 6, 2022 05:47
-
-
Save vicksbr/0bc56bda0dbf5050ee67b2659657fb92 to your computer and use it in GitHub Desktop.
usePortal with hooks
This file contains hidden or 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
| // THIS WORK IS NOT MINE | |
| // WORK FROM: https://www.jayfreestone.com/writing/react-portals-with-hooks/ | |
| // just gist it for the sake of not lose it. | |
| // ty fellow developer!! | |
| import React, { useRef, useEffect } from 'react'; | |
| /** | |
| * Creates DOM element to be used as React root. | |
| * @returns {HTMLElement} | |
| */ | |
| function createRootElement(id) { | |
| const rootContainer = document.createElement('div'); | |
| rootContainer.setAttribute('id', id); | |
| return rootContainer; | |
| } | |
| /** | |
| * Appends element as last child of body. | |
| * @param {HTMLElement} rootElem | |
| */ | |
| function addRootElement(rootElem) { | |
| 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. | |
| */ | |
| function usePortal(id) { | |
| const rootElemRef = useRef(null); | |
| useEffect(function setupElement() { | |
| // 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 function removeElement() { | |
| rootElemRef.current.remove(); | |
| if (!parentElem.childElementCount) { | |
| parentElem.remove(); | |
| } | |
| }; | |
| }, [id]); | |
| /** | |
| * 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(); | |
| } | |
| export default usePortal; | |
| // usage | |
| import React from 'react'; | |
| import { createPortal } from 'react-dom'; | |
| import usePortal from './usePortal'; | |
| /** | |
| * @example | |
| * <Portal id="modal"> | |
| * <p>Thinking with portals</p> | |
| * </Portal> | |
| */ | |
| const Portal = ({ id, children }) => { | |
| const target = usePortal(id); | |
| return createPortal( | |
| children, | |
| target, | |
| ); | |
| }; | |
| export default Portal; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment