cssreactjstypescriptnext.jsreact-portal

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


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


Solution

  • 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;