eMoosavi
Weekly frontend dev braindumps
Unleashing the Power of React Portals for Flexible UI Components
React Components

Unleashing the Power of React Portals for Flexible UI Components

Master advanced UI patterns with the unexpected flexibility of React Portals

Jul 03, 2024 - 12:356 min read

Unleashing the Power of React Portals for Flexible UI Components

React Portals are one of those advanced but often overlooked features of React. This post dives deep into the concept of React Portals, showing how to use them effectively to build flexible UI components that transcend the ordinary.

React Portals provide a first-class way to render children into a DOM node that exists outside the parent component's DOM hierarchy.

What are React Portals?

By default, React components render children into their DOM hierarchy. However, there are times when you need to render a child into a different DOM node, like creating a modal, tooltip, or any other UI element that should break out of the parent container.

This is where React Portals come into play.

Here's a simple example of a React Portal:

import React from 'react';
import ReactDOM from 'react-dom';

function MyPortal({ children }) {
  return ReactDOM.createPortal(
    children,
    document.getElementById('portal-root')
  );
}

In this snippet, ReactDOM.createPortal accepts two arguments:

  1. The first is the JSX elements to render.
  2. The second is the DOM node where you want to render those elements.

Creating a Simple Modal with React Portals

Let's enhance the previous example to build a simple modal component using React Portals.

Step 1: Setup the HTML structure

Make sure your index.html file has the following structure:

<div id="root"></div>
<div id="portal-root"></div>

Step 2: Create the Modal Component

Here, we create a Modal component using React Portals:

import React from 'react';
import ReactDOM from 'react-dom';
import './Modal.css'; // Assuming you have some CSS for the modal

function Modal({ isOpen, onClose, children }) {
  if (!isOpen) return null;

  return ReactDOM.createPortal(
    <div className="modal-overlay">
      <div className="modal">
        <button onClick={onClose} className="modal-close">&times;</button>
        {children}
      </div>
    </div>,
    document.getElementById('portal-root')
  );
}

export default Modal;

In this Modal component:

  • We check if isOpen is true. If not, we return null to avoid rendering anything.
  • If isOpen is true, we use ReactDOM.createPortal to render the modal content inside the portal-root element.

Step 3: Using the Modal Component

Now, let's use the Modal component in our application.

import React, { useState } from 'react';
import Modal from './Modal';

function App() {
  const [isModalOpen, setIsModalOpen] = useState(false);

  return (
    <div className="App">
      <button onClick={() => setIsModalOpen(true)}>Open Modal</button>
      <Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
        <h1>Hello from the Modal!</h1>
      </Modal>
    </div>
  );
}

export default App;

In this App component:

  • We manage the state of the modal using useState hook.
  • When the button is clicked, we set isModalOpen to true, making the modal visible.
  • We pass isModalOpen and onClose handler to the Modal component.

This approach keeps our modal logic and structure clean and separate from the rest of the application, adhering to React's declarative nature.

Enhancing the Modal

Closing the Modal on Click Outside

To enhance the UX, let's add a feature where clicking outside the modal closes it.

Update the Modal component:

function Modal({ isOpen, onClose, children }) {
  if (!isOpen) return null;

  const handleOutsideClick = (e) => {
    if (e.target.className === 'modal-overlay') {
      onClose();
    }
  };

  return ReactDOM.createPortal(
    <div className="modal-overlay" onClick={handleOutsideClick}>
      <div className="modal">
        <button onClick={onClose} className="modal-close">&times;</button>
        {children}
      </div>
    </div>,
    document.getElementById('portal-root')
  );
}

Now, when the user clicks outside the modal (modal-overlay), the onClose function is called, closing the modal.

Accessibility Considerations

Accessibility (a11y) must be a priority for any web application. To make our modal more accessible, let's add some ARIA attributes.

Update the Modal component again:

function Modal({ isOpen, onClose, children }) {
  if (!isOpen) return null;

  const handleOutsideClick = (e) => {
    if (e.target.className === 'modal-overlay') {
      onClose();
    }
  };

  return ReactDOM.createPortal(
    <div className="modal-overlay" onClick={handleOutsideClick} role="dialog" aria-modal="true">
      <div className="modal">
        <button onClick={onClose} className="modal-close" aria-label="Close Modal">&times;</button>
        {children}
      </div>
    </div>,
    document.getElementById('portal-root')
  );
}
  • We added role="dialog" to the modal-overlay.
  • We added aria-modal="true" to the modal-overlay.
  • We added aria-label="Close Modal" to the close button.

These changes make it clearer to screen readers that this is a dialog (modal) and what its purpose is.

Code Splitting with Dynamic Imports

While not exclusive to Portals, code-splitting is a performance optimization technique that pairs well with them. Often, modals can be a secondary part of the user experience and may not need to be loaded upfront. Instead, you can dynamically import them when needed.

Here's how you can leverage dynamic imports with React Portals:

Dynamic Import of Modal

import React, { useState, Suspense, lazy } from 'react';

const Modal = lazy(() => import('./Modal'));

function App() {
  const [isModalOpen, setIsModalOpen] = useState(false);

  return (
    <div className="App">
      <button onClick={() => setIsModalOpen(true)}>Open Modal</button>
      <Suspense fallback={<div>Loading...</div>}>
        {isModalOpen && (
          <Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
            <h1>Hello from the Modal!</h1>
          </Modal>
        )}
      </Suspense>
    </div>
  );
}

export default App;

By using React.lazy and Suspense, we load the modal component only when it's needed. This improves the initial load time of our application.

Portals for Tooltips and Dropdowns

While modals are a common use case for portals, they are not the only one. Tooltips and dropdowns also benefit greatly from portals, as they often need to break out of their parent's stacking context.

Tooltip Example

Let's create a simple tooltip component using portals.

import React from 'react';
import ReactDOM from 'react-dom';
import './Tooltip.css';

function Tooltip({ text, targetRef }) {
  const { top, left, height } = targetRef.current.getBoundingClientRect();

  return ReactDOM.createPortal(
    <div className="tooltip" style={{ top: top + height + window.scrollY, left: left + window.scrollX }}>
      {text}
    </div>,
    document.body
  );
}

export default Tooltip;

In this Tooltip component:

  • We get the position of the target element using getBoundingClientRect.
  • We calculate the position for the tooltip and use ReactDOM.createPortal to render it inside document.body.

Using the Tooltip Component

import React, { useRef, useState } from 'react';
import Tooltip from './Tooltip';

function App() {
  const buttonRef = useRef(null);
  const [isTooltipVisible, setIsTooltipVisible] = useState(false);

  return (
    <div className="App">
      <button
        ref={buttonRef}
        onMouseEnter={() => setIsTooltipVisible(true)}
        onMouseLeave={() => setIsTooltipVisible(false)}
      >
        Hover me
      </button>
      {isTooltipVisible && <Tooltip text="This is a tooltip" targetRef={buttonRef} />}
    </div>
  );
}

export default App;

In this App component:

  • We manage the visibility of the tooltip using useState.
  • We use useRef to get the position of the button element and pass it to the Tooltip component.

This keeps the tooltip logic simple and detached from its parent, rendering it where it naturally belongs in the DOM hierarchy.

Conclusion

React Portals provide a powerful way to build flexible and detached UI components that cannot be achieved easily with traditional React rendering techniques. By mastering React Portals, you can create sophisticated UIs with modals, tooltips, dropdowns, and any other components that need to render outside their parent DOM hierarchy.

Experiment with Portals and combine them with other React features like React.lazy and Suspense for performance optimizations. And always keep accessibility in mind to ensure your applications are usable by all.

Happy coding!

Article tags
reactportalsui-componentsaccessibilityoptimization
Previous article

Web Performance

Leveraging Intersection Observer API with React for Enhanced Performance

Next article

React Hooks

Crafting Dynamic Forms with React Hook Form and Yup