DEV Community

Cover image for How to Build a React Portal Manager for Dynamic Modals and Tooltips
HexShift
HexShift

Posted on • Edited on

How to Build a React Portal Manager for Dynamic Modals and Tooltips

React Portals let you render components outside the main DOM tree — perfect for modals, tooltips, and dropdowns. But managing dozens of portals manually gets messy fast. Here's how to create a clean Portal Manager system in React without external libraries.

Why Use a Portal Manager?

Common use cases:

  • Centralized control over all modals, tooltips, and popovers
  • Dynamic stacking, layering, and closing logic
  • Cleaner separation of UI layers

Step 1: Build a Portal Host

This component will be your centralized container for all portals:

// PortalHost.js
import { createContext, useContext, useState } from "react";
import { createPortal } from "react-dom";

const PortalContext = createContext(null);

export function PortalHost({ children }) {
  const [portals, setPortals] = useState([]);

  const mount = (node) => {
    const id = Math.random().toString(36).substr(2, 9);
    setPortals((prev) => [...prev, { id, node }]);
    return id;
  };

  const unmount = (id) => {
    setPortals((prev) => prev.filter((p) => p.id !== id));
  };

  return (
    <PortalContext.Provider value={{ mount, unmount }}>
      {children}
      {portals.map(({ id, node }) =>
        createPortal(node, document.body)
      )}
    </PortalContext.Provider>
  );
}

export function usePortal() {
  return useContext(PortalContext);
}

Step 2: Create a Hook for Dynamic Portals

We’ll build a simple hook to open and close portals:

// useDynamicPortal.js
import { usePortal } from "./PortalHost";
import { useEffect, useRef } from "react";

export function useDynamicPortal(content) {
  const { mount, unmount } = usePortal();
  const idRef = useRef(null);

  useEffect(() => {
    idRef.current = mount(content);
    return () => unmount(idRef.current);
  }, [content, mount, unmount]);
}

Step 3: Use It to Create a Modal Dynamically

Example of launching a modal when clicking a button:

// ExampleModal.js
import { useState } from "react";
import { useDynamicPortal } from "./useDynamicPortal";

function ModalContent({ onClose }) {
  return (
    <div style={{ position: "fixed", top: "40%", left: "40%", background: "white", padding: "2rem", zIndex: 1000 }}>
      <p>I'm a modal!</p>
      <button onClick={onClose}>Close</button>
    </div>
  );
}

function ExampleModal() {
  const [open, setOpen] = useState(false);

  if (open) {
    useDynamicPortal(<ModalContent onClose={() => setOpen(false)} />);
  }

  return <button onClick={() => setOpen(true)}>Open Modal</button>;
}

export default ExampleModal;

Step 4: Wrap the App with the PortalHost

// App.js
import { PortalHost } from "./PortalHost";
import ExampleModal from "./ExampleModal";

function App() {
  return (
    <PortalHost>
      <ExampleModal />
    </PortalHost>
  );
}

export default App;

Pros and Cons

✅ Pros

  • Centralized portal management
  • No external libraries needed
  • Clean separation of modal logic

⚠️ Cons

  • More setup compared to a simple createPortal use
  • Limited layering control without manual z-indexing

🚀 Alternatives

  • React-Aria or Radix UI for more structured UI primitives
  • Headless UI modals if you want battle-tested accessibility

Summary

When your React app grows, managing multiple popups, tooltips, and modals becomes chaotic. A Portal Manager brings clean order to the chaos, and this simple implementation is a solid foundation to customize even further for complex use cases.

For a much more extensive guide on getting the most out of React portals, check out my full 24-page PDF file on Gumroad. It's available for just $10:

Using React Portals Like a Pro.

If you found this useful, you can support me here: buymeacoffee.com/hexshift

Top comments (0)