♻️ Use shared singleton containers for React portals (#8957)

Refactor use-portal-container to allocate one persistent <div> per
logical category (:modal, :popup, :tooltip, :default) instead of
creating a new div for every component instance. This keeps the DOM
clean with at most four fixed portal containers and eliminates the
arbitrary growth of empty <div> elements on document.body while
preserving the removeChild race condition fix.
This commit is contained in:
Andrey Antukh 2026-04-14 10:48:30 +02:00 committed by GitHub
parent b3645658fb
commit c39609b991
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 33 additions and 15 deletions

View File

@ -160,7 +160,7 @@
tooltip-ref (mf/use-ref nil)
container (hooks/use-portal-container)
container (hooks/use-portal-container :tooltip)
id
(d/nilv id internal-id)

View File

@ -380,17 +380,35 @@
state))
(defn- get-or-create-portal-container
"Returns the singleton container div for the given category, creating
and appending it to document.body on first access."
[category]
(let [body (dom/get-body)
id (str "portal-container-" category)]
(or (dom/query body (str "#" id))
(let [container (dom/create-element "div")]
(dom/set-attribute! container "id" id)
(dom/append-child! body container)
container))))
(defn use-portal-container
"Creates a dedicated div container for React portals. The container
is appended to document.body on mount and removed on cleanup, preventing
removeChild race conditions when multiple portals target the same body."
[]
(let [container (mf/use-memo #(dom/create-element "div"))]
(mf/with-effect []
(let [body (dom/get-body)]
(dom/append-child! body container)
#(dom/remove-child! body container)))
container))
"Returns a shared singleton container div for React portals, identified
by a logical category. Available categories:
:modal — modal dialogs
:popup — popups, dropdowns, context menus
:tooltip — tooltips
:default — general portal use (default)
All portals in the same category share one <div> on document.body,
keeping the DOM clean and avoiding removeChild race conditions."
([]
(use-portal-container :default))
([category]
(let [category (name category)]
(mf/with-memo [category]
(get-or-create-portal-container category)))))
(defn use-dynamic-grid-item-width
([] (use-dynamic-grid-item-width nil))

View File

@ -84,7 +84,7 @@
(mf/defc modal-container*
{::mf/props :obj}
[]
(let [container (hooks/use-portal-container)]
(let [container (hooks/use-portal-container :modal)]
(when-let [modal (mf/deref ref:modal)]
(mf/portal
(mf/html [:> modal-wrapper* {:data modal :key (dm/str (:id modal))}])

View File

@ -517,7 +517,7 @@
dropdown-direction-change* (mf/use-ref 0)
top (+ (get-in mdata [:position :y]) 5)
left (+ (get-in mdata [:position :x]) 5)
container (hooks/use-portal-container)]
container (hooks/use-portal-container :popup)]
(mf/use-effect
(mf/deps is-open?)

View File

@ -36,7 +36,7 @@
dropdown-direction-change* (mf/use-ref 0)
top (+ (get-in mdata [:position :y]) 5)
left (+ (get-in mdata [:position :x]) 5)
container (hooks/use-portal-container)
container (hooks/use-portal-container :popup)
delete-node (mf/use-fn
(mf/deps mdata)

View File

@ -114,7 +114,7 @@
:is-open? true
:rect rect))))))
container (hooks/use-portal-container)]
container (hooks/use-portal-container :popup)]
[:div {:on-click on-open-dropdown
:disabled (not can-edit?)