What are React Portals and when would you use them?

4 minintermediatereactportals

Quick Answer

React Portals provide a way to render children into a DOM node that exists outside the parent component's DOM hierarchy. This is useful for modals, tooltips, dropdowns, and other UI elements that need to break out of their container's styling constraints.

Detailed Answer

What are React Portals and when would you use them?

Answer:

React Portals provide a way to render children into a DOM node that exists outside the parent component's DOM hierarchy. This is useful for modals, tooltips, dropdowns, and other UI elements that need to break out of their container's styling constraints.

Basic Portal Implementation:

import { createPortal } from 'react-dom';

interface ModalProps {
  isOpen: boolean;
  onClose: () => void;
  children: React.ReactNode;
}

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

  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        <button className="modal-close" onClick={onClose}>×</button>
        {children}
      </div>
    </div>,
    document.body // Portal target
  );
}

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

  return (
    <div>
      <button onClick={() => setIsModalOpen(true)}>
        Open Modal
      </button>
      
      <Modal 
        isOpen={isModalOpen} 
        onClose={() => setIsModalOpen(false)}
      >
        <h2>Modal Title</h2>
        <p>Modal content goes here...</p>
      </Modal>
    </div>
  );
}

Advanced Portal with Dynamic Container:

function Portal({ children, containerId = 'portal-root' }: { 
  children: React.ReactNode; 
  containerId?: string;
}) {
  const [container, setContainer] = useState<HTMLElement | null>(null);

  useEffect(() => {
    // Find or create portal container
    let portalContainer = document.getElementById(containerId);
    
    if (!portalContainer) {
      portalContainer = document.createElement('div');
      portalContainer.id = containerId;
      document.body.appendChild(portalContainer);
    }
    
    setContainer(portalContainer);
    
    // Cleanup
    return () => {
      if (portalContainer && portalContainer.parentNode) {
        portalContainer.parentNode.removeChild(portalContainer);
      }
    };
  }, [containerId]);

  if (!container) return null;

  return createPortal(children, container);
}

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

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

  return (
    <>
      <div
        ref={triggerRef}
        onMouseEnter={() => {
          updatePosition();
          setIsVisible(true);
        }}
        onMouseLeave={() => setIsVisible(false)}
      >
        {children}
      </div>
      
      <Portal>
        {isVisible && (
          <div
            style={{
              position: 'absolute',
              top: position.top,
              left: position.left,
              background: 'black',
              color: 'white',
              padding: '4px 8px',
              borderRadius: '4px',
              zIndex: 1000,
              pointerEvents: 'none'
            }}
          >
            {content}
          </div>
        )}
      </Portal>
    </>
  );
}

Portal with Focus Management:

function AccessibleModal({ isOpen, onClose, children }: ModalProps) {
  const modalRef = useRef<HTMLDivElement>(null);
  const previousActiveElement = useRef<HTMLElement | null>(null);

  useEffect(() => {
    if (isOpen) {
      // Store the currently focused element
      previousActiveElement.current = document.activeElement as HTMLElement;
      
      // Focus the modal
      modalRef.current?.focus();
      
      // Prevent body scroll
      document.body.style.overflow = 'hidden';
      
      // Handle escape key
      const handleEscape = (e: KeyboardEvent) => {
        if (e.key === 'Escape') {
          onClose();
        }
      };
      
      document.addEventListener('keydown', handleEscape);
      
      return () => {
        document.removeEventListener('keydown', handleEscape);
        document.body.style.overflow = 'unset';
        
        // Restore focus
        previousActiveElement.current?.focus();
      };
    }
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  return createPortal(
    <div 
      className="modal-overlay"
      onClick={onClose}
      role="dialog"
      aria-modal="true"
    >
      <div
        ref={modalRef}
        className="modal-content"
        onClick={(e) => e.stopPropagation()}
        tabIndex={-1}
      >
        {children}
      </div>
    </div>,
    document.body
  );
}

When to Use Portals:

  1. Modals and Dialogs - Break out of parent container styling
  2. Tooltips and Popovers - Position relative to viewport
  3. Dropdowns - Avoid z-index and overflow issues
  4. Notifications - Render at app level
  5. Loading Overlays - Cover entire application

Portal Best Practices:

  1. Always clean up - Remove portal containers when unmounting
  2. Handle focus management - For accessibility
  3. Use proper z-index - Ensure portals appear above other content
  4. Consider SSR - Portals don't work during server-side rendering
  5. Test with screen readers - Ensure accessibility
// Custom hook for portal management
function usePortal(containerId: string) {
  const [container, setContainer] = useState<HTMLElement | null>(null);

  useEffect(() => {
    let portalContainer = document.getElementById(containerId);
    
    if (!portalContainer) {
      portalContainer = document.createElement('div');
      portalContainer.id = containerId;
      document.body.appendChild(portalContainer);
    }
    
    setContainer(portalContainer);
    
    return () => {
      if (portalContainer?.parentNode) {
        portalContainer.parentNode.removeChild(portalContainer);
      }
    };
  }, [containerId]);

  const Portal = useCallback(({ children }: { children: React.ReactNode }) => {
    if (!container) return null;
    return createPortal(children, container);
  }, [container]);

  return Portal;
}

// Usage
function MyComponent() {
  const Portal = usePortal('my-portal');
  
  return (
    <div>
      <Portal>
        <div>This renders in a portal!</div>
      </Portal>
    </div>
  );
}