Creating a portal in Next Js differs from creating a portal in React Js. React Js generally run on the client side, while Next Js run on the server side. My modal renders on the client side. Therefore, in Next Js, I must ensure that the DOM is ready and the container is available to mount the portal.
Here are the steps to create a portal in Next Js.
- Create a _document.js file
- Create a DOM node in the _document.js to mount the portal
- Mount the portal to the DOM node
I use Typescript. You should too if possible. So my file extension is tsx.
1. Create a _document.js file
My _document.tsx
resides in the pages folder. The _document.tsx
is the equivalent of the index.html
in React Js. If you don't have a _document.tsx
, create one inside the pages
folder.
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
2. Create a div element in the _document.js
I created a div <div id="portal" />
in the _document.tsx
. That is where I mount my portal.
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html>
<Head />
<body>
<Main />
<div id="portal" />
<NextScript />
</body>
</Html>
)
}
3. Mount the portal to the DOM node
Not it is the time to create a component to create a portal and mount it to the DOM node.
import { useRef, useEffect, useState, ReactNode } from 'react'
import { createPortal } from 'react-dom'
import styles from "./Overlay.module.css"
interface PortalProps {
children: ReactNode
}
export const Portal = (props: OverlayProps) => {
const ref = useRef<Element | null>(null)
const [mounted, setMounted] = useState(false)
useEffect(() => {
ref.current = document.querySelector<HTMLElement>("#portal")
setMounted(true)
}, [])
return (mounted && ref.current) ? createPortal(<div className={styles.overlay}>{props.children}</div>, ref.current) : null
}
Here are the styles to go with the portal
.overlay {
display: block;
position: fixed;
padding-top: 100px;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.4);
z-index: 2;
}
I am doing a few things differently here. I use a ref to hold the DOM node. That ensures I get the same reference to the Node in every re-render.
I use the state variable mounted to ensure my component is mounted and usEffect
is executed. Then I use both those conditions before creating the portal. That is important.
The usage of this component is easy.
<Portal>
<p>Next Js Portal by LearnBestCoding</p>
</Portal>