Cheese on Toast with React Portals?
Andy Coupe
Posted on April 20, 2020
Portals allow elements to sit within the React component tree but render to an alternative container in the DOM.
This can be useful when we want to render elements such as modals, tooltips, toast notifications from anywhere within our React application.
Also, events inside a portal will propagate to ancestors in the containing React tree, even if those elements are not ancestors in the DOM tree.
Even though a portal can be anywhere in the DOM tree, it behaves like a normal React child in every other way. Features like context work exactly the same regardless of whether the child is a portal, as the portal still exists in the React tree regardless of position in the DOM tree.
I used a simple code sandbox to create this mini tutorial/explanation which can be found at the end of this post.
Create our portal-root
The portal root is going to be an empty div which sits alongside our React root element.
Open your index.html
file and create your portal-root
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<div id="portal-root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
Cool. Save the file and be done with it.
Create our ToastPortal
component.
const ToastPortal = ({ children }) => {
// Find our portal container in the DOM
const portalRoot = document.getElementById("portal-root");
/*
Create a div as a wrapper for our toast
using the useMemo hook so that a new value isn't
computed on every render
*/
const toastContainer = React.useMemo(() => document.createElement("div"), []);
React.useEffect(() => {
/*
Append our toast container to the portal root
*/
portalRoot.appendChild(toastContainer);
/*
Clean up the DOM by removing our toast container
when the component is unmounted
*/
return () => {
toastContainer.remove();
};
});
/*
Render any child elements to the portal root
*/
return createPortal(children, portalRoot);
};
Render the cheese on toast
Now let's put our portal to use by rendering a classic in most Michelin Star restaurants, cheese on toast. Replace the code inside your App
component with the following.
export default function App() {
const [isToastVisible, setIsToastVisible] = React.useState(false);
const [inputValue, setInputValue] = React.useState("Hi");
const handleClick = () => setIsToastVisible(!isToastVisible);
const handleChange = ({ target }) => setInputValue(target.value);
return (
<div className="App">
<input value={inputValue} onChange={handleChange} />
<button onClick={handleClick}>{isToastVisible ? "Close" : "Open"}</button>
{isToastVisible && (
<ToastPortal>
<div
style={{
position: "fixed",
top: 8,
right: 8,
backgroundColor: "pink",
borderRadius: 8,
padding: 8
}}
>
<span role="img" aria-label="cheese on toast">
🧀
</span>
on toast
{inputValue}
</div>
</ToastPortal>
)}
</div>
);
}
The toast notification is rendered outside of our React application but still has the ability to interact with our application state. 😎
This is a decent use case for implementing a custom usePortal hook. Try it out!
Conclusion
Hopefully this has given you an insight into how portals work and the flexibility they can provide. Next time you want to render a modal, tooltip, sidebar nav etc - maybe you could reach out to React Portals.
Posted on April 20, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.