Skip to content

[React 19] Regression when using createPortal with DOM element created by dangerouslySetInnerHTML #31600

@jonathanhefner

Description

@jonathanhefner

In React 18, it was possible to use createPortal with a DOM element created by dangerouslySetInnerHTML.

Example (adapted from this Stack Overflow answer):

import { useCallback, useState } from "react";
import { createPortal } from "react-dom";

export default function App() {
  const htmlFromElsewhere = `foo <span class="portal-container"></span> bar`;

  return <InnerHtmlWithPortals html={htmlFromElsewhere} />
}

function InnerHtmlWithPortals({ html }: { html: string }) {
  const [portalContainer, setPortalContainer] = useState<Element | null>(null)

  const refCallback = useCallback((el: HTMLDivElement) => {
    setPortalContainer(el?.querySelector(".portal-container"))
  })

  return <>
    <div ref={refCallback} dangerouslySetInnerHTML={{ __html: html }} />
    {portalContainer && createPortal(<Cake />, portalContainer)}
  </>
}

function Cake() {
  return <strong>cake</strong>
}

React 18 CodeSandbox: https://codesandbox.io/p/sandbox/optimistic-kowalevski-73sk5w

In React 19, this no longer works. React appears to be re-rendering the inner HTML after calling refCallback. Thus, createPortal succeeds, but the portalContainer element that it uses is no longer part of the DOM.

React 19 CodeSandbox: https://codesandbox.io/p/sandbox/vibrant-cloud-gd8yzr

It is possible to work around the issue by setting innerHTML directly instead of using dangerouslySetInnerHTML:

   const [portalContainer, setPortalContainer] = useState<Element | null>(null)

   const refCallback = useCallback((el: HTMLDivElement) => {
+    if (el) el.innerHTML = html
     setPortalContainer(el?.querySelector(".portal-container"))
-  })
+  }, [html])

   return <>
-    <div ref={refCallback} dangerouslySetInnerHTML={{ __html: html }} />
+    <div ref={refCallback} />
     {portalContainer && createPortal(<Cake />, portalContainer)}
   </>

But I'm not sure whether that is a reliable solution.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions