ReactDOM.createPortal() is creating extra blank divs in next.js-typescript

2.4k Views Asked by At

this is backdrop.tsx:

interface BacdropProps {
  open?: string;
  onClick: () => void;
}

const Backdrop: React.FC<BacdropProps> = (props) => {
  let container: HTMLDivElement | null = null;
  if (typeof window !== "undefined") {
    const rootContainer = document.createElement("div");
    const parentElem = document.querySelector("#__next");
    parentElem?.insertAdjacentElement("afterend", rootContainer);
    // parentElem?.after(rootContainer) this gives me same issue
    container = rootContainer;
  }

  return container
    ? ReactDOM.createPortal(
        <div
          className={["backdrop", props.open ? "open" : ""].join(" ")}
          onClick={props.onClick}
        />,
        container
      )
    : null;
};

export default Backdrop;

this is css for Backdoor.tsx

.backdrop {
  width: 100%;
  height: 100vh;
  background: rgba(0, 0, 0, 0.75);
  z-index: 100;
  position: fixed;
  left: 0;
  top: 0;
  transition: opacity 0.3s ease-out;
  opacity: 1;
}

this is how it looks: enter image description here

1

There are 1 best solutions below

2
On BEST ANSWER

Your code will create div.backdrop every time when Backdrop re-render. The correct way should be create it once. The correct way is using useEffect to promise ReactDOM.createPortal just be executed once. And also apply the useRef to make sure container to keep the same instance in every render.

const containerRef = useRef<HTMLDivElement>(null);

useEffect({
  // Will be execute once in client-side
  if (typeof window !== "undefined") {
    const rootContainer = document.createElement("div");
    const parentElem = document.querySelector("#__next");
    parentElem?.insertAdjacentElement("afterend", rootContainer);
    // parentElem?.after(rootContainer) this gives me same issue
    containerRef.current = rootContainer;
  }
}, [window])

useEffect({
  // Will be execute once when containerRef is bind to <HTMLDivElement>
  if(containerRef.current) {
    ReactDOM.createPortal(
      <div
        className={["backdrop", props.open ? "open" : ""].join(" ")}
        onClick={props.onClick}
      />,
      containerRef.current
    )
  }
}, [containerRef])

Edit

  1. I removed the detection of existence in window, since useEffect would be executed only in client-side.

  2. Since ReactDOM.createPortal will create the div.backdrop outside of the root HTMLElement (div#next), i think just return null in Backdrop component is fine.

const containerRef = useRef<HTMLDivElement>(null);

useEffect({
  // useEffect would run just in client-side
  const rootContainer = document.createElement("div");
  const parentElem = document.querySelector("#__next");
  parentElem?.insertAdjacentElement("afterend", rootContainer);
  // parentElem?.after(rootContainer) this gives me same issue
  containerRef.current = rootContainer;
}, [])

useEffect({
  // Will be execute once when containerRef is bind to <HTMLDivElement>
  if(containerRef.current) {
    ReactDOM.createPortal(
      <div
        className={["backdrop", props.open ? "open" : ""].join(" ")}
        onClick={props.onClick}
      />,
      containerRef.current
    )
  }
}, [containerRef])

return null;