Skip to content

Instantly share code, notes, and snippets.

@vicksbr
Last active July 6, 2022 05:47
Show Gist options
  • Select an option

  • Save vicksbr/0bc56bda0dbf5050ee67b2659657fb92 to your computer and use it in GitHub Desktop.

Select an option

Save vicksbr/0bc56bda0dbf5050ee67b2659657fb92 to your computer and use it in GitHub Desktop.
usePortal with hooks
// 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