What Are Portals in React?
In this blog, we'll cover React Portals, uses cases, code examples, caveats interview questions, and more!
Kaushal Joshi
Dec 03, 2024 • 11 min read
In today's blog, we will explore an interesting concept in React. This concept breaks some of the fundamental rules of React. Although it has been part of the library since React 16.0, many misunderstandings persist even today.
We are talking about Portals in React. In this blog, we’ll dive deep into React portals: what they are, why they’re needed, how they work, and where you can use them effectively. We'll also include a real-world code example, some caveats to keep in mind and interview questions to solidify your understanding.
Prerequisites and Assumptions
This blog covers an advanced concept in React. While writing the blog, I've already assumed that you are well aware of the following:
React: Understanding of component rendering, the virtual DOM, and parent-child component relationships.
CSS: Concepts like z-index, stacking context, absolute/relative positioning, and overflow handling.
JavaScript: Familiarity with event bubbling and propagation.
Furthermore, I've used TypeScript and Tailwind CSS in code examples. It's not mandatory to know them, and you won't get stuck if you don't know them,
With that in mind, let's get started...
What Are Portals in React?
A Portal in React provides a way to render a child into a DOM node that exists outside the parent DOM hierarchy of that component.
By default, React components render inside their parent DOM. However, there are scenarios where rendering them elsewhere in the DOM is necessary. Portals solve this problem while maintaining React's declarative component structure and event bubbling model.
In the following sections, let’s explore why this is needed, scenarios for its use, and how to implement it.
Why Do We Need Portals in React?
In React applications, components are rendered into their parent DOM nodes. As every React component is rendered inside the parent div, the structure of the components in the React tree directly mirrors the resulting DOM tree.
However, there are some limitations to this approach. As elements are rendered as children of their parent components, they are restricted by CSS stylings set by the parent element.
Z-index and stacking context issues
Components like modals, dialog boxes, and notifications must appear on top of the screen, regardless of how deeply nested they are within the DOM hierarchy. However, because of the z-index of parent elements, they might not appear on top of the screen.
For example,
export default function App() {
return (
<div className="relative z-10">
<p>I am supposed to be below the modal</p>
<Modal />
</div>
)
}
const Modal = () => (
<div className="absolute z-50 bg-white p-5 rounded-lg">
This is a modal.
</div>
)
Here, the modal has a high z-index
, but if its parent container has a lower stacking context, the modal might still appear behind other elements.
Styling and positioning issues
If a parent component applies restrictive styles such as overflow:hidden
, it can clip child components. For example:
const Container = () => (
<div className="w-80 h-15 overflow-hidden border">
<Tooltip />
</div>
);
const Tooltip = () => (
<div className="absolute top-12 left-12 bg-gray-50">
This is a tooltip
</div>
);
This would render in the physical DOM like the following:
<div class="w-80 h-15 overflow-hidden border">
<div class="absolute top-12 left-12 bg-gray-50">
This is a tooltip
</div>
</div>
Even though the tooltip is styled with absolute
position, as the parent's overflow is set to hidden
, it won't be displayed on the screen.
How Do React Portal Works?
Portal allows you to render a component in a different node. Let's consider the tooltip example we just saw. The problem is that as the parent element has overflow set to hidden, hence the child component with absolute positioning is not being displayed properly.
What if there was a way that would allow the tooltip to render inside a different DOM node? If the different DOM node doesn't have restricted stylings (like overflow: hidden
), there would be no problem with the rendering of the tooltip. However, while doing so, we want to keep the component's behavior as same as before, as if it is still rendering inside its original parent.
That's exactly what React Portal does. It allows you to render a component in a different DOM node so that it's not restricted by its parent stylings.
Portals solve a niche problem with how React works internally: rendering components outside their natural DOM hierarchy without compromising state management and event bubbling.
Building a Portal Component
The Portal is part of the react-dom
library that handles all the logic and implementation of rendering React components on the browser's DOM. It provides a createPortal()
method that allows us to create a portal in React.
The createPortal()
method takes three arguments:
Children: The React component is to be rendered.
DOM Node: The DOM node where the component will be rendered.
Key: An optional argument (string or number) that acts as a unique identifier of the portal.
When React renders a portal, it injects the children into the specified DOM node, bypassing the parent DOM hierarchy. Let's visualize this with a small example.
Suppose there's an <App />
component that has three nested components: <Navbar />
, <Page />
, and <Footer />
. The <Page />
component is rendering a Modal component inside it. For reference, here's how the Modal component is written.
import { createPortal } from 'react-dom';
const Modal = ({ children }) => {
return createPortal(
children,
document.querySelector("#modal-root")
);
};
This is how both React's Virtual DOM as well as the physical DOM would interpret the position of the modal in the DOM hierarchy:
For React,
<Modal />
is still the child of the<Page />
component. Hence it will behave exactly like any other component. That means, event bubbling, and event propagation would work the same way.For the browser's DOM, the
<Modal />
must render inside a div with an id ofmodal-root
. Hence, it won't be restricted by any other HTML element and can be rendered independently.
Use Cases of Portals in React
Here are some of the scenarios where portals are useful:
Modals and Dialogs: These often require rendering at the root level (alongside
#app
) to avoid any clipping issues caused by CSSoverflow
orz-index
.Tooltips and Popovers: These often need to be rendered so that they are relative to the viewport or another element outside the parent container. Portals render such components eliminating the need to implement complex logic.
Dropdowns: Sometimes dropdowns are rendered improperly if the parent styles have
overflow: hidden
. In such cases, Portals provide a way to render a component without being constrained by its parent's styling.Toast components: Displaying notification messages/alerts at a global level without interfering with the component that triggers this event.
Any component that might render inappropriately as it is restricted by the styling, stacking context, or positioning of its parent, should be rendered using Portals
Creating a Modal Component with Portals in React
Let's get our hands dirty and see a practical example of how Portals work in React. We'll create a simple Modal component that renders on the same level as the root element.
We'll use React, TypeScript, and Tailwind CSS. It's totally fine if you are not using TypeScript or Tailwind. Everything would work exactly the same.
Setting up a modal component
Setting up a Portal is as simple as wrapping a component with createPortal()
and provide the destination DOM element. Let's create a modal example to get a better understanding of this.
import { createPortal } from 'react-dom';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}
const Modal= ({ isOpen, onClose, children }: ModalProps) => {
if(!isOpen) return <></>
return createPortal(
<div className="fixed top-0 left-0 right-0 bottom-0 bg-black opacity-70 flex justify-center items-center">
<div className="bg-white p-5 rounded-lg max-w-md">
<button onClick={onClose} className="absolute top-2 right-2 text-red-500 hover:text-red-600">
Close
</button>
{children}
</div>
</div>,
document.getElementById("modal-root")
);
}
export default Modal;
Here we are attaching the modal to an element with #modal-root
id. As an alternative, you can attach it to the root element (#app
) or the <body>
tag as well. Having a separate div element makes it easy to debug if a bug occurs.
Adding the modal-root
to HTML
In the last section, we attached the modal to #modal-root
. Now let's create that element in our index file.
In public/index.html
, add an element with the id of modal-root
.
<div id="root"></div>
<div id="modal-root"></div>
Using the modal component
Now it's time to use the component. Inside your App component, paste the following code:
import Modal from "@/components/ui/modal"
export default function App() {
const [isModalOpen, setIsModalOpen] = React.useState(false);
return (
<>
<div>
<h1>Welcome to React Portals Demo!</h1>
<button onClick={() => setIsModalOpen(true)}>Open Modal</button>
</div>
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
<h2>Modal Content</h2>
<p>This modal is rendered using a React portal!</p>
<p>Open browser's devtools and see the DOM hierarchy to get surprised!</p>
</Modal>
</>
);
}
Congratulations! You have created a modal component using React Portal! Go to Inspect the code and see where the modal has rendered.
Caveats of React Portals
Although Portals are incredibly powerful, there are some important things to know while using them:
Event bubbling: Events from portals propagate according to the React tree rather than the DOM tree. We'll cover this in detail in the next section.
CSS styling: If you are rendering a component at the root level, alongside
#app
, you might require global styles.Server-Side Rendering support: If you’re using server-side rendering (SSR), ensure the portal's container (
#modal-root
) exists on the server and client to avoid hydration mismatches.Z-Index Conflicts: Ensure that the portal content has proper
z-index
values to appear on top of other elements.
Event Bubbling With React Portals
As discussed in the last section, event bubbling propagates according to the React tree. Although the component is physically at a different DOM level, for React's virtual DOM, it's still a child of the same parent.
Events triggered inside a portal propagate through React’s virtual DOM tree, NOT the physical DOM tree.
This behavior ensures React's virtual DOM reconciliation works seamlessly even for components rendered outside their natural DOM hierarchy.
Code example: Event bubbling with Portals
Let's see an example to demonstrate how event bubbling works with portals.
import { useState } from "react";
import Modal from "./modal";
export default function App() {
const [isModalOpen, setIsModalOpen] = useState(false);
const handleClick = () => alert("Clicked on ParentComponent");
return (
<>
<div
onClick={handleClick}
className="p-5 bg-purple-500 border"
>
<p>Click anywhere inside this Parent Component</p>
<button onClick={() => setIsModalOpen(true)}>Open Modal</button>
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
<h2>Modal Content</h2>
<p>This modal is rendered using a React portal!</p>
<p>
Open browser's devtools and see the DOM hierarchy to get surprised!
</p>
<button onClick={() => alert("Clicked on the modal button")}>Click me!</button>
</Modal>
</div>
</>
);
}
Now,
Click on the
Open Modal
button.Once the modal opens, click on the
Click me!
button.
Surprised? Let's dissect this behavior.
The
<App />
contains<Modal />
in its React tree, even though<Modal />
is rendered outside the parent DOM.Hence, when you click the
Open Modal
button, the click event propagates to the parent and triggers theonClick()
event of the parent as well.Furthermore, when you click the
Click me!
button from inside the modal, theonClick()
event for the portal is triggered first. And then, the same event for its parent component is called.
This representation would help you understand how Portal is treated in React's virtual DOM as well as physical DOM.
To avoid this, use e.stopPropagation()
inside the portal component:
onClick={(e) => {
e.stopPropagation();
alert("Clicked on the modal button");
}}
Interview Questions
Now that you have a deep knowledge of Portals, it's time to prepare for the interviews.
What are Portals in React? How do they work?
How do components built with Portals behave on physical and virtual DOMs? Explain with simple diagrams.
What method is used to create a Portal? Explain its parameters.
Explain event bubbling with Portals.
Provide practical use cases of using Portals in React.
How would you handle server-side rendering (SSR) while working with Portals?
Explain why React uses virtual DOM bubbling over physical DOM bubbling. Highlight how this ensures consistency in complex UI.
We haven’t covered the last two topics in this article, though we’ve briefly touched on them. It’d be a nice stretch for your brain to find their answers.
Assignment Time!
We covered the what, why, how, and when of React Portals in this article. I am confident that this article helped you grasp a solid understanding of React Portals. Now, to verify your learnings, I have a small assignment for you!
While learning about styling and positioning issues in the Why Do We Need Portals in React? section, we saw an example of how the tooltip component was restricted by its parent's positioning. As an assignment, use what we learned today and build a tooltip component using React Portals!
Once done, feel free to share it with me or on your socials. You can post on Peerlist or Twitter, where I am usually active the most.
If you are looking for a new job, or want to share projects you built over the weekends, you must check out Peerlist!
Until then, happy coding! 👩💻