♻️ Extract use-portal-container hook to reduce duplication (#8798)

The dedicated-container portal pattern was repeated across 6 components.
Extract it into a reusable use-portal-container hook under app.main.ui.hooks.
This commit is contained in:
Andrey Antukh
2026-03-26 12:45:42 +01:00
committed by GitHub
parent 85cfb8161a
commit 5fca9457cf
7 changed files with 36 additions and 16 deletions

View File

@@ -6,16 +6,12 @@
(ns app.main.ui.components.portal
(:require
[app.util.dom :as dom]
[app.main.ui.hooks :as hooks]
[rumext.v2 :as mf]))
(mf/defc portal-on-document*
[{:keys [children]}]
(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)))
(let [container (hooks/use-portal-container)]
(mf/portal
(mf/html [:* children])
container)))

View File

@@ -9,6 +9,7 @@
[app.main.style :as stl])
(:require
[app.common.data :as d]
[app.main.ui.hooks :as hooks]
[app.util.dom :as dom]
[app.util.keyboard :as kbd]
[app.util.timers :as ts]
@@ -159,6 +160,8 @@
tooltip-ref (mf/use-ref nil)
container (hooks/use-portal-container)
id
(d/nilv id internal-id)
@@ -283,4 +286,4 @@
[:div {:class (stl/css :tooltip-content)} content]
[:div {:class (stl/css :tooltip-arrow)
:id "tooltip-arrow"}]]])
(.-body js/document)))]))
container))]))

View File

@@ -380,6 +380,18 @@
state))
(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))
(defn use-dynamic-grid-item-width
([] (use-dynamic-grid-item-width nil))
([itemsize]

View File

@@ -10,6 +10,7 @@
[app.common.data.macros :as dm]
[app.main.data.modal :as modal]
[app.main.store :as st]
[app.main.ui.hooks :as hooks]
[app.util.dom :as dom]
[app.util.keyboard :as k]
[goog.events :as events]
@@ -83,7 +84,8 @@
(mf/defc modal-container*
{::mf/props :obj}
[]
(when-let [modal (mf/deref ref:modal)]
(mf/portal
(mf/html [:> modal-wrapper* {:data modal :key (dm/str (:id modal))}])
(dom/get-body))))
(let [container (hooks/use-portal-container)]
(when-let [modal (mf/deref ref:modal)]
(mf/portal
(mf/html [:> modal-wrapper* {:data modal :key (dm/str (:id modal))}])
container))))

View File

@@ -20,6 +20,7 @@
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.hooks :as hooks]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.timers :as timers]
@@ -515,7 +516,8 @@
dropdown-direction (deref dropdown-direction*)
dropdown-direction-change* (mf/use-ref 0)
top (+ (get-in mdata [:position :y]) 5)
left (+ (get-in mdata [:position :x]) 5)]
left (+ (get-in mdata [:position :x]) 5)
container (hooks/use-portal-container)]
(mf/use-effect
(mf/deps is-open?)
@@ -554,4 +556,4 @@
:on-context-menu prevent-default}
(when mdata
[:& token-context-menu-tree (assoc mdata :width @width :on-delete-token on-delete-token)])]])
(dom/get-body)))))
container))))

View File

@@ -6,6 +6,7 @@
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.hooks :as hooks]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[okulary.core :as l]
@@ -35,6 +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)
delete-node (mf/use-fn
(mf/deps mdata)
@@ -80,4 +82,4 @@
:type "button"
:on-click delete-node}
(tr "labels.delete")]]])]])
(dom/get-body)))))
container))))

View File

@@ -17,6 +17,7 @@
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.main.ui.hooks :as hooks]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[cuerdas.core :as str]
@@ -111,7 +112,9 @@
(let [rect (dom/get-bounding-rect node)]
(swap! state* assoc
:is-open? true
:rect rect))))))]
:rect rect))))))
container (hooks/use-portal-container)]
[:div {:on-click on-open-dropdown
:disabled (not can-edit?)
@@ -140,4 +143,4 @@
[:& theme-options {:active-theme-paths active-theme-paths
:themes themes
:on-close on-close-dropdown}]]])
(dom/get-body)))]))
container))]))