⚡ Add xf:add-index and memoize interactions menu rendering (#11915)

Introduce a shared xf:add-index transducer in app.common.data that
attaches the position to each item, and cover it with unit tests.

Use it in the workspace interactions menu: the indexed interactions
list is now derived in a memoized step keyed on the interactions
prop, so it is not rebuilt when the section is collapsed or
expanded. The previous code called d/enumerate on every render.

Update the frontend UI conventions memory with the pattern and the
constraint that the transducer only works on associative items.

AI-assisted-by: deepseek-v4.1-flash
This commit is contained in:
Andrey Antukh 2026-09-24 18:09:49 +02:00 committed by GitHub
parent cdb0b3950d
commit de14311ce7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 35 additions and 1 deletions

View File

@ -33,6 +33,7 @@
## Performance
- Keep expensive derived data in refs, memoized selectors, or pure helpers. In hot render paths, prefer existing `app.common.data.macros` helpers where local code already uses them.
- Derive index-aware or sorted/filtered sequences once: wrap the transformation in `mf/with-memo` keyed on the source collection instead of calling `d/enumerate` in the render body. Use the shared `d/xf:add-index`, which attaches `:app.common.data/index` to each item; the items must be associative (maps/records), so it does not work on keywords or plain ids.
- Avoid creating new callback functions/objects inside hot renders when a named function, memoized callback, data attribute, or precomputed JS props object works.
- Destructure props/state values used repeatedly. Avoid repeated deref/property access in render loops.

View File

@ -41,6 +41,7 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def xf:map-id (map :id))
(def xf:add-index (map-indexed (fn [index item] (assoc item ::index index))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Structures

View File

@ -1031,3 +1031,31 @@
;; Clamp out-of-range positions
(t/is (= [:b :c :d :a] (d/reorder [:a :b :c :d] 0 100)))
(t/is (= [:a :b :c :d] (d/reorder [:a :b :c :d] -5 0))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Commonly used transducers
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(t/deftest xf-add-index-adds-index-to-each-item
(t/is (= [{:id 1 ::d/index 0}
{:id 2 ::d/index 1}
{:id 3 ::d/index 2}]
(into [] d/xf:add-index [{:id 1} {:id 2} {:id 3}]))))
(t/deftest xf-add-index-preserves-input-order
(t/is (= [0 1 2]
(mapv ::d/index (into [] d/xf:add-index [{:id :c} {:id :a} {:id :b}])))))
(t/deftest xf-add-index-handles-empty-and-nil-input
(t/is (= [] (into [] d/xf:add-index [])))
(t/is (= [] (into [] d/xf:add-index nil))))
(t/deftest xf-add-index-composes-with-other-transducers
;; The index is assigned after previous stages run, so it always
;; reflects the position in the output of the chain.
(t/is (= [{:id 1 :keep? true ::d/index 0}
{:id 3 :keep? true ::d/index 1}]
(into [] (comp (filter :keep?) d/xf:add-index)
[{:id 1 :keep? true}
{:id 2 :keep? false}
{:id 3 :keep? true}]))))

View File

@ -710,6 +710,10 @@
(let [show-content* (mf/use-state true)
show-content? (deref show-content*)
interactions
(mf/with-memo [interactions]
(into [] d/xf:add-index interactions))
toggle-content
(mf/use-fn
#(swap! show-content* not))
@ -745,7 +749,7 @@
(when show-content?
[:div {:class (stl/css :content :content-interactions)}
(for [[index interaction] (d/enumerate interactions)]
(for [{:keys [::d/index] :as interaction} interactions]
[:> interaction-item* {:key (str (:id shape) "-" index)
:index index
:shape shape