How to Update React Portals in React

React Portals provide a powerful way to render children into a DOM node that exists outside the DOM hierarchy of the parent component. This article explains how to dynamically update the state, props, and content of React Portals, ensuring seamless communication and synchronization between the portal and your main React application.

Understanding React Portal Updates

Although a portal is rendered into a different physical DOM node (often at the end of the <body> tag), it still behaves like a standard React child in every other way. It remains positioned inside the React virtual DOM tree.

Because the portal remains in the React tree, updating a React Portal is identical to updating any standard React component. It retains access to the React Context, receives props, and triggers re-renders whenever the parent component’s state changes.

1. Updating Portals via Parent State and Props

The most common way to update a portal is by passing dynamic props or state from the parent component. When the parent component’s state changes, React automatically re-renders the portal with the updated data.

Here is an example of a modal portal that updates its content based on the parent component’s state:

import React, { useState } from 'react';
import ReactDOM from 'react-dom';

// The Portal Component
const ModalPortal = ({ isOpen, message, onClose }) => {
  if (!isOpen) return null;

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

// The Parent Component
export default function App() {
  const [isOpen, setIsOpen] = useState(false);
  const [counter, setCounter] = useState(0);

  return (
    <div className="app">
      <h1>React Portal Update Example</h1>
      <button onClick={() => setIsOpen(true)}>Open Portal</button>
      <button onClick={() => setCounter(counter + 1)}>Increment Counter ({counter})</button>

      {/* The Portal updates automatically when 'counter' or 'isOpen' changes */}
      <ModalPortal 
        isOpen={isOpen} 
        message={`The current count is: ${counter}`} 
        onClose={() => setIsOpen(false)} 
      />
    </div>
  );
}

In this example, every time the “Increment Counter” button is clicked, the parent state counter updates. React propagates this change down to the ModalPortal prop message, forcing the portal to update its visible DOM content immediately.

2. Updating Parent State from inside the Portal (Event Bubbling)

Even though a portal is rendered outside the parent’s DOM node, events fired from inside the portal still bubble up through the React virtual DOM tree.

This means you can capture events triggered inside the portal at the parent level to update your application state.

import React, { useState } from 'react';
import ReactDOM from 'react-dom';

const ClickPortal = () => {
  return ReactDOM.createPortal(
    <div className="portal-button-container">
      <button>Click Me to Trigger Parent Event</button>
    </div>,
    document.getElementById('portal-root')
  );
};

export default function App() {
  const [clicks, setClicks] = useState(0);

  // This handler catches clicks from the portal due to React event bubbling
  const handlePortalClick = () => {
    setClicks((prev) => prev + 1);
  };

  return (
    <div onClick={handlePortalClick} className="parent-container">
      <p>Portal button clicks captured by parent: {clicks}</p>
      <ClickPortal />
    </div>
  );
}

Because of React’s event system, clicking the button inside ClickPortal bubbles up to the div wrapper in App, updating the clicks state and subsequently updating the UI.

3. Dynamically Changing the Portal Destination

In rare scenarios, you might need to move the portal from one DOM element to another dynamically. You can achieve this by storing the target DOM node in a state variable and passing it to ReactDOM.createPortal.

import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';

const DynamicDestinationPortal = ({ children }) => {
  const [targetNode, setTargetNode] = useState(null);

  useEffect(() => {
    // Dynamically choose or change target destination based on conditions
    const target = document.getElementById('dynamic-container-id') || document.body;
    setTargetNode(target);
  }, []);

  if (!targetNode) return null;

  return ReactDOM.createPortal(children, targetNode);
};

By managing the target DOM container within React’s lifecycle, the portal will tear itself down from the old DOM location and remount in the new one whenever the targetNode state is updated.