Now You’re Thinking With (React JS) Portals

Photo by David Holifield on Unsplash

Now You’re Thinking With (React JS) Portals

Braincuber Technologies's photo
Braincuber Technologies
·Mar 22, 2022·

8 min read

Subscribe to my newsletter and never miss my upcoming articles

Hello [insert engineer name], and welcome to the React Development Enrichment Center. Please respond in the affirmative if you’ve you ever experienced the following:

  • Installed an entire bloated component library just to implement a simple modal
  • Created a tooltip system entirely from CSS that you were embarrassed to deploy
  • Cried to the point of exhaustion over the implications of state management for a pop-up form nested deeply within your application

If any of the above scenarios triggered an intense reaction of PTSD in your brain, you’re in luck. In the following test chamber, you will be introduced to portals, a simple and effective tool for your React repertoire that will no doubt leave you jumping for joy the next time a frontend ticket comes your way.

Do be sure to proceed all the way to the end of the test chamber. There will be cake. Author’s Note: Okay, no more dated Portal references, I promise.

What Are React JS Portals?

Portals are an incredibly useful feature in React that allow developers to easily render child components to the DOM somewhere outside of their immediate parent tree. When used correctly, they’re an excellent tool for simplifying certain components that tend to be otherwise messy or obtuse in their implementations within the context of React.

Modals, tooltips, and other “pop-up” style features are common use cases for portals. These types of floating components are often classic pain points for React developers, as managing the required state across sometimes several nested components on the DOM tree can be a struggle, and tying down the component specifically to the parent that’s controlling its state and properties is often less than ideal.

Let’s put together a simple snack bar type alert component complete with a custom hook for state management in order to dive into how portals work.

Opening the Portal

The first thing we’ll want to do to get things going is set up our portal. Let’s pop open our document’s public index.html file. If you used create-react-app to scaffold your project, this file will have already been autogenerated and populated with most of what we need. Scroll down a bit until you find a div with a “root” ID. This is the DOM element upon which React renders our app to in index.js. Every component that descends from the top level of our application will typically live inside of this container. Let’s create another div element directly below the root and give this one an ID of “portal.” If you haven’t already guessed, this is what we’ll hook into to render our alert!

<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no"
    />
    <meta name="theme-color" content="#000000" />
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <title>React App</title>
  </head>
  <body>
    <noscript>
      You need to enable JavaScript to run this app.
    </noscript>
    <div id="root"></div>
    <div id="portal"></div> <!-- Here's our portal! -->
  </body>
</html>

Next up, we’ll actually wire into it. Let’s create a file called useAlert.jsx — this is where we’ll build our alert component itself, as well as the custom hook we’ll use to render it.

// useAlert.jsx
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";

const Alert = ({ alertVisible, alertRendered, alertText }) => {
  const portalElement = document.getElementById("portal");
  const ele = (
    <div className="alert-container">
      {alertRendered && alertText && (
        <div className={`alert ${alertVisible ? "open" : ""}`}>             
          {alertText}
        </div>
      )}
    </div>
  );

  return portalElement 
    ? ReactDOM.createPortal(ele, portalElement)
    : null;
};
export default Alert;

We’ll keep things simple to start off and create a basic component that takes in two state variables - alertVisible and alertRendered - which will eventually control the render state of the alert along with a simple transition, as well as an alertText string to render inside. Next, we’ll find the portal div we created in our index with a simple document query. Once we’ve written the JSX for the actual component, we’ll return a call to the ReactDOM.createPortal() method, passing in first what we want to slap onto the portal (our alert component), and then the portal element itself. After that, our portal is ready to roll! To test it out, let’s throw in a bit of styling and hook up some state in our App.js component.

// App.js
import { useState } from "react";
import Alert from "./useAlert";
import { content } from "./content";
import "./styles.css";
export default function App() {
  const [isVisible, setIsVisible] = useState(false);
  const [isRendered, setIsRendered] = useState(false);
  const [alertText, setAlertText] = useState("");
  const renderAlert = (text) => {
    setAlertText(text);
    setTimeout(() => {
      setIsVisible(true);
    }, 0);
    setIsRendered(true);
    setTimeout(() => {
      setTimeout(() => {
        setIsRendered(false);
      }, 250);
      setIsVisible(false);
    }, 3000);
  };
return (
    <>
      <Alert
        alertVisible={isVisible}
        alertRendered={isRendered}
        alertText={alertText}
      />
      <div className="App">
        <p>{content}</p>
        <button
          onClick={() => renderAlert("Hey, something happened!")}
        >
          Trigger Alert
        </button>
      </div>
    </>
  );
}

We’ll set up some variables with useState to handle rendering and transitioning the alert, as well as a renderAlert function that takes in the text we want to display and actually triggers the event. Let’s slap it on a button and give it a shot:

react-js-portal.gif

Awesome! We’ve successfully taken a component that was called inside of our application and rendered it on an entirely different section of the DOM by using portals! The beauty of this method is that it doesn’t matter where we drop our Alert within the component tree — it will always consistently render outside and independent of any ties to the styling or hierarchy of its parents. Pretty cool, right?

At this point, you might be thinking something along the lines of “yeah sure it’s neat and all, but what’s it really accomplishing if I still need to manage its state in the parent?” Great question! It certainly does feel like in some ways we’re defeating the purpose of the portal by tying the component down to a specific point on the tree in this way. Furthermore, with this approach we’ll need to be constantly rewriting our state control variables as well as the renderAlert function itself in any parent that needs it. Let’s fix that by abstracting our work out into a custom hook!

// useAlert.jsx
import React, { createContext, useContext, useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const AlertContext = createContext();
export const AlertProvider = ({ children }) => {
  const [isVisible, setIsVisible] = useState(false);
  const [isRendered, setIsRendered] = useState(false);
  const [alertText, setAlertText] = useState("");
  return (
    <AlertContext.Provider
      value={{
        isVisible,
        setIsVisible,
        isRendered,
        setIsRendered,
        alertText,
        setAlertText
      }}
    >
      {children}
    </AlertContext.Provider>
  );
};
export const useAlert = () => {
  const context = useContext(AlertContext);
  if (context === undefined)
    throw new Error("useAlert must be called within an   
    AlertProvider");
  const {
    isVisible,
    setIsVisible,
    isRendered,
    setIsRendered,
    alertText,
    setAlertText
  } = context;
  const renderAlert = (text) => {
    setAlertText(text);
    setTimeout(() => {
      setIsVisible(true);
    }, 0);
    setIsRendered(true);
    setTimeout(() => {
      setTimeout(() => {
        setIsRendered(false);
      }, 250);
      setIsVisible(false);
    }, 3000);
  };
  return {
    renderAlert,
    isVisible,
    isRendered,
    alertText
  };
};
const Alert = () => {
  const { isVisible, isRendered, alertText } = useAlert();
  const portalElement = document.getElementById("portal");
  const ele = (
    <div className="alert-container">
      {isRendered && alertText && (
        <div className={`alert ${isVisible ? "open" : ""}`}>   
          {alertText}
        </div>
      )}
    </div>
  );
return portalElement 
  ? ReactDOM.createPortal(ele, portalElement)
  : null;
};
export default Alert;

Boom! From the top, we’ll first employ React context to create what is more or less a “single source of truth” for our Alert component’s state. I’m not going to get into the details of context here, but essentially what we’re doing is defining our necessary state inside of the AlertProvider component, and then returning a container that we can wrap around our entire app which will provide that state to all of its children as its context value.

Next up, inside of the useAlert hook, we’ll grab that context and first check that the developer did indeed implement the aforementioned provider. If they didn’t, we’ll remind them with a friendly error. Otherwise, we’ll destructure out all of our state from the context, and then use it to abstract away the logic that we were previously hardcoding into the DOM tree. Users will be able to access the renderAlert trigger function using the familiar hook syntax, as we’ll see shortly.

Finally, we’ll update the Alert component itself by removing the need for props passed in from the parent, instead gathering that state information through the useAlert hook we just created. And that’s it! Let’s take a look at how painless our component now is to use in practice:

// index.js
import ReactDOM from "react-dom";
import { AlertProvider } from "./useAlert";
import App from "./App";
const rootElement = document.getElementById("root");
  ReactDOM.render(
    <AlertProvider>
      <App />
    </AlertProvider>,
  rootElement
);

First, wrap your entire app in the AlertProvider. Then, through the magic of portals, hooks and context…

// App.js
import Alert, { useAlert } from "./useAlert";
import { content } from "./content";
import "./styles.css";
export default function App() {
  const { renderAlert } = useAlert();
return (
    <>
      <Alert />
      <div className="App">
        <p>{content}</p>
        <button
          onClick={() => renderAlert("Hey, something happened!")}
        >
          Trigger Alert
        </button>
      </div>
    </>
  );
}

Look at that! Our code now looks markedly more clean, and we’ve removed the need to repeat our state declarations every time we want to use our component across the app. It’s now as easy as simply importing the component and the hook, slapping the component anywhere in our JSX we feel like, then grabbing renderAlert from the hook and calling it with our desired text. Much better.

Wrapping Up

React portals are a great way decouple certain elements from your application’s component tree, and when used in tandem with custom hooks can be an effective tool for cleaning up and further abstracting your code. Rejoice! You can now look back and laugh (or maybe cry) about the time you installed the entirety of Material UI into your portfolio repository just to add a single modal to your website that one time. Totally not speaking from experience or anything.

Read More

The JavaScript framework war is over or not

Best JavaScript important codes

Did you find this article valuable?

Support Braincuber Technologies by becoming a sponsor. Any amount is appreciated!

Learn more about Hashnode Sponsors
 
Share this