-
Notifications
You must be signed in to change notification settings - Fork 49.8k
Open
Labels
Description
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.
gkiely, avlaguta and devrnd