♻️ Extract use-portal-container hook to reduce duplication

The dedicated-container portal pattern was repeated across 7 components.
Extract it into a reusable use-portal-container hook under app.main.ui.hooks.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
Andrey Antukh 2026-03-23 13:47:33 +00:00
parent ff60503ce6
commit 2905905a9f
8 changed files with 26 additions and 41 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,7 +160,7 @@
tooltip-ref (mf/use-ref nil)
container (mf/use-memo #(dom/create-element "div"))
container (hooks/use-portal-container)
id
(d/nilv id internal-id)
@ -246,11 +247,6 @@
content
aria-label)})]
(mf/with-effect []
(let [body (dom/get-body)]
(dom/append-child! body container)
#(dom/remove-child! body container)))
(mf/use-effect
(mf/deps tooltip-id)
(fn []

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

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

View File

@ -19,6 +19,7 @@
[app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.forms :as fc]
[app.main.ui.hooks :as hooks]
[app.main.ui.workspace.tokens.management.forms.controls.combobox-navigation :refer [use-navigation]]
[app.main.ui.workspace.tokens.management.forms.controls.floating-dropdown :refer [use-floating-dropdown]]
[app.main.ui.workspace.tokens.management.forms.controls.token-parsing :as tp]
@ -92,7 +93,7 @@
icon-button-ref (mf/use-ref nil)
ref (or ref internal-ref)
container (mf/use-memo #(dom/create-element "div"))
container (hooks/use-portal-container)
raw-tokens-by-type (mf/use-ctx muc/active-tokens-by-type)
@ -269,11 +270,6 @@
(mf/with-effect [dropdown-options]
(mf/set-ref-val! options-ref dropdown-options))
(mf/with-effect []
(let [body (dom/get-body)]
(dom/append-child! body container)
#(dom/remove-child! body container)))
(mf/with-effect [is-open* ref wrapper-ref]
(when is-open
(let [handler (fn [event]

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,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 (mf/use-memo #(dom/create-element "div"))
container (hooks/use-portal-container)
delete-node (mf/use-fn
(mf/deps mdata)
@ -45,11 +46,6 @@
(when node
(on-delete-node node type)))))]
(mf/with-effect []
(let [body (dom/get-body)]
(dom/append-child! body container)
#(dom/remove-child! body container)))
(mf/with-effect [is-open?]
(when (and (not= 0 (mf/ref-val dropdown-direction-change*)) (= false is-open?))
(reset! dropdown-direction* "down")

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]
@ -113,12 +114,7 @@
:is-open? true
:rect rect))))))
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 (hooks/use-portal-container)]
[:div {:on-click on-open-dropdown
:disabled (not can-edit?)