Portals in React

Loading

Portals provide a way to render children into a DOM node that exists outside the DOM hierarchy of the parent component. This is particularly useful for modals, tooltips, and other elements that need to “break out” of their container.

1. Basic Portal Implementation

Creating a Portal

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

function Modal({ children }) {
  return ReactDOM.createPortal(
    <div className="modal">
      {children}
    </div>,
    document.getElementById('modal-root')
  );
}

// Usage
function App() {
  const [showModal, setShowModal] = React.useState(false);

  return (
    <div>
      <button onClick={() => setShowModal(true)}>Show Modal</button>
      {showModal && (
        <Modal>
          <h2>Modal Title</h2>
          <p>Modal content goes here</p>
          <button onClick={() => setShowModal(false)}>Close</button>
        </Modal>
      )}
    </div>
  );
}

HTML Setup

<!-- In your public/index.html -->
<div id="root"></div>
<div id="modal-root"></div>

2. Advanced Portal Techniques

Dynamic Portal Root Creation

function usePortal(id) {
  const [portalRoot, setPortalRoot] = React.useState(null);

  React.useEffect(() => {
    let element = document.getElementById(id);
    let created = false;

    if (!element) {
      element = document.createElement('div');
      element.setAttribute('id', id);
      document.body.appendChild(element);
      created = true;
    }

    setPortalRoot(element);

    return () => {
      if (created && element.parentNode) {
        element.parentNode.removeChild(element);
      }
    };
  }, [id]);

  return portalRoot;
}

function Portal({ id, children }) {
  const target = usePortal(id);
  return target ? ReactDOM.createPortal(children, target) : null;
}

Event Bubbling Through Portals

function ParentComponent() {
  const handleClick = () => {
    console.log('Clicked in parent, even though event originated in portal');
  };

  return (
    <div onClick={handleClick}>
      <p>Regular content</p>
      <Modal>
        <button>Click me (inside portal)</button>
      </Modal>
    </div>
  );
}

3. Common Use Cases

Modal Dialogs

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

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

Tooltips

function Tooltip({ children, content }) {
  const [isVisible, setIsVisible] = React.useState(false);
  const triggerRef = React.useRef(null);
  const [position, setPosition] = React.useState({ top: 0, left: 0 });

  const updatePosition = () => {
    if (triggerRef.current) {
      const rect = triggerRef.current.getBoundingClientRect();
      setPosition({
        top: rect.bottom + window.scrollY,
        left: rect.left + window.scrollX
      });
    }
  };

  React.useEffect(() => {
    if (isVisible) {
      updatePosition();
      window.addEventListener('resize', updatePosition);
      return () => window.removeEventListener('resize', updatePosition);
    }
  }, [isVisible]);

  return (
    <>
      <span
        ref={triggerRef}
        onMouseEnter={() => setIsVisible(true)}
        onMouseLeave={() => setIsVisible(false)}
      >
        {children}
      </span>

      {isVisible && ReactDOM.createPortal(
        <div 
          className="tooltip"
          style={{
            position: 'absolute',
            top: `${position.top}px`,
            left: `${position.left}px`
          }}
        >
          {content}
        </div>,
        document.body
      )}
    </>
  );
}

Loading Indicators

function GlobalLoadingIndicator({ isLoading }) {
  return isLoading ? ReactDOM.createPortal(
    <div className="global-loader">
      <div className="spinner"></div>
    </div>,
    document.getElementById('loading-root')
  ) : null;
}

4. Best Practices

  1. Accessibility:
  • For modals, implement proper focus management
  • Add ARIA attributes for screen readers
  • Handle keyboard navigation (Escape to close, etc.)
  1. Performance:
  • Reuse portal roots when possible
  • Avoid frequent mounting/unmounting of portal content
  • Consider lazy-loading portal content
  1. Styling:
  • Use fixed or absolute positioning for overlay elements
  • Ensure proper z-index stacking
  • Handle body scroll locking for modals
  1. Testing:
   test('portal renders correctly', () => {
     const modalRoot = document.createElement('div');
     modalRoot.setAttribute('id', 'modal-root');
     document.body.appendChild(modalRoot);

     render(<Modal>Test Content</Modal>);

     expect(modalRoot).toHaveTextContent('Test Content');

     document.body.removeChild(modalRoot);
   });

5. Advanced Patterns

Context in Portals

const ThemeContext = React.createContext('light');

function ThemedPortal() {
  return (
    <ThemeContext.Provider value="dark">
      <Modal>
        <ThemeContext.Consumer>
          {theme => <div className={`theme-${theme}`}>Content</div>}
        </ThemeContext.Consumer>
      </Modal>
    </ThemeContext.Provider>
  );
}

Nested Portals

function NestedPortals() {
  return (
    <Portal id="first-portal">
      <div>First level portal content</div>
      <Portal id="second-portal">
        <div>Second level portal content</div>
      </Portal>
    </Portal>
  );
}

Server-Side Rendering (SSR)

function SafePortal({ id, children }) {
  const [isMounted, setIsMounted] = React.useState(false);

  React.useEffect(() => {
    setIsMounted(true);
  }, []);

  if (!isMounted) return null;

  let portalRoot = document.getElementById(id);
  if (!portalRoot) {
    portalRoot = document.createElement('div');
    portalRoot.setAttribute('id', id);
    document.body.appendChild(portalRoot);
  }

  return ReactDOM.createPortal(children, portalRoot);
}

6. Troubleshooting

Common Issues and Solutions

  1. Portal content not appearing:
  • Verify the target DOM node exists before rendering
  • Check for typos in the target node ID
  • Ensure the portal is mounted (especially with SSR)
  1. Event handling problems:
  • Remember events bubble through the React tree, not the DOM tree
  • Use event.stopPropagation() when needed
  1. Styling issues:
  • Make sure portal content isn’t hidden by parent overflow
  • Verify z-index values are appropriate
  • Check for conflicting position values

Portals are a powerful feature that enable you to break out of the normal DOM hierarchy while maintaining React’s component structure and context. They’re particularly valuable for UI elements that need to visually “escape” their container constraints while remaining logically part of your React component tree.

Leave a Reply

Your email address will not be published. Required fields are marked *