♻️ 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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 36 additions and 16 deletions

View File

@ -6,16 +6,12 @@
(ns app.main.ui.components.portal (ns app.main.ui.components.portal
(:require (:require
[app.util.dom :as dom] [app.main.ui.hooks :as hooks]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(mf/defc portal-on-document* (mf/defc portal-on-document*
[{:keys [children]}] [{:keys [children]}]
(let [container (mf/use-memo #(dom/create-element "div"))] (let [container (hooks/use-portal-container)]
(mf/with-effect []
(let [body (dom/get-body)]
(dom/append-child! body container)
#(dom/remove-child! body container)))
(mf/portal (mf/portal
(mf/html [:* children]) (mf/html [:* children])
container))) container)))

View File

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

View File

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

View File

@ -10,6 +10,7 @@
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.hooks :as hooks]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.keyboard :as k] [app.util.keyboard :as k]
[goog.events :as events] [goog.events :as events]
@ -83,7 +84,8 @@
(mf/defc modal-container* (mf/defc modal-container*
{::mf/props :obj} {::mf/props :obj}
[] []
(when-let [modal (mf/deref ref:modal)] (let [container (hooks/use-portal-container)]
(mf/portal (when-let [modal (mf/deref ref:modal)]
(mf/html [:> modal-wrapper* {:data modal :key (dm/str (:id modal))}]) (mf/portal
(dom/get-body)))) (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.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.hooks :as hooks]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[app.util.timers :as timers] [app.util.timers :as timers]
@ -515,7 +516,8 @@
dropdown-direction (deref dropdown-direction*) dropdown-direction (deref dropdown-direction*)
dropdown-direction-change* (mf/use-ref 0) dropdown-direction-change* (mf/use-ref 0)
top (+ (get-in mdata [:position :y]) 5) 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/use-effect
(mf/deps is-open?) (mf/deps is-open?)
@ -554,4 +556,4 @@
:on-context-menu prevent-default} :on-context-menu prevent-default}
(when mdata (when mdata
[:& token-context-menu-tree (assoc mdata :width @width :on-delete-token on-delete-token)])]]) [:& 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.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.hooks :as hooks]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[okulary.core :as l] [okulary.core :as l]
@ -35,6 +36,7 @@
dropdown-direction-change* (mf/use-ref 0) dropdown-direction-change* (mf/use-ref 0)
top (+ (get-in mdata [:position :y]) 5) 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)
delete-node (mf/use-fn delete-node (mf/use-fn
(mf/deps mdata) (mf/deps mdata)
@ -80,4 +82,4 @@
:type "button" :type "button"
:on-click delete-node} :on-click delete-node}
(tr "labels.delete")]]])]]) (tr "labels.delete")]]])]])
(dom/get-body))))) container))))

View File

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