🐛 Fix problems with react loops (#11941)

This commit is contained in:
Alonso Torres 2026-09-28 16:59:06 +02:00 committed by GitHub
parent b890b94d27
commit c0714def01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 105 additions and 87 deletions

View File

@ -1473,12 +1473,17 @@
(rx/filter (complement :skip-component-sync?))
;; Keep waits pending while component changes are checked.
(rx/map start-sync-barrier)
(rx/observe-on :async))
(rx/share))
get-component-events
(fn [[event old-data]]
(let [{:keys [file-id changes save-undo? undo-group]} event
changed-components
;; Buffers commits until a timer turn passes with no new ones. The
;; debounce timer (not a microtask) queues behind any timers already
;; due, so a burst of commits is inspected as one batch in one task.
commit-batches-s
(rx/buffer-until (rx/debounce 0 commits-s) commits-s)
get-component-changes
(fn [old-data {:keys [file-id changes save-undo? undo-group]}]
(let [changed-components
(when (and old-data
(or (nil? file-id) (= file-id (:id old-data))))
(into #{}
@ -1486,29 +1491,39 @@
changes))]
(cond
(empty? changed-components)
(rx/empty)
nil
save-undo?
(do
(log/info :hint "detected component changes"
:ids (map str changed-components)
:undo-group undo-group)
(->> (rx/from changed-components)
(rx/map #(component-changed
% (:id old-data) undo-group))))
(map #(vector ::component-changed % undo-group)
changed-components))
:else
;; Undos only bump :modified-at.
(->> (rx/from changed-components)
(rx/map touch-component)))))
(map #(vector ::touch-component %) changed-components))))
;; One event per distinct component change in the batch
get-component-events
(fn [batch old-data]
(->> batch
(mapcat #(get-component-changes old-data (first %)))
(distinct)
(map (fn [[type component-id undo-group]]
(if (= type ::component-changed)
(component-changed component-id (:id old-data) undo-group)
(touch-component component-id))))
(rx/from)))
component-events-s
(->> commits-s
(->> commit-batches-s
(rx/with-latest-from workspace-buffer-s)
(rx/mapcat
(fn [[[event task] old-data]]
(->> (get-component-events [event old-data])
(rx/finalize #(finish-sync-barrier! task)))))
(fn [[batch old-data]]
(->> (get-component-events batch old-data)
(rx/finalize #(run! (comp finish-sync-barrier! second) batch)))))
;; Close barriers left behind when the page shuts down.
(rx/finalize #(wrf/finish-tasks! @pending-sync-barriers*))
(rx/share))

View File

@ -202,12 +202,6 @@
file-id (mf/use-ctx ctx/current-file-id)
current-colors* (mf/use-state [])
current-colors (deref current-colors*)
grouped-colors* (mf/use-state {})
grouped-colors (deref grouped-colors*)
open-groups* (mf/use-state #{})
open-groups (deref open-groups*)
@ -221,13 +215,15 @@
[{:value "recent" :label (tr "workspace.libraries.colors.recent-colors") :id "recent"}
{:value "file" :label (tr "workspace.libraries.colors.file-library") :id "file"}])
;; Equal-stable, since `select*` resets its selection when options change
options
(mf/with-memo [library-options libraries file-id]
(into library-options
(comp
(map val)
(map (fn [lib] {:value (d/name (:id lib)) :label (:name lib) :id (d/name (:id lib))})))
(dissoc libraries file-id)))
(h/use-equal-memo
(mf/with-memo [library-options libraries file-id]
(into library-options
(comp
(map val)
(map (fn [lib] {:value (d/name (:id lib)) :label (:name lib) :id (d/name (:id lib))})))
(dissoc libraries file-id))))
on-library-change
(mf/use-fn
@ -281,49 +277,49 @@
(fn [s]
(if (contains? s path)
(disj s path)
(conj s path))))))]
(conj s path))))))
;; Load library colors when the selected library (or filter options) change.
;;
;; flat current-colors* -- used for the grid view and the recent list view.
;; grouped grouped-colors* -- used for the library grouped list view.
;;
;; Library colors are fully converted with `library-color->color` here so
;; the render path never needs to do it. `flat-colors` is materialised as
;; an eager vector so realisation does not leak into render time.
;; open-groups* is reset to #{} (all groups expanded) on every library switch.
(mf/with-effect [selected recent-colors libraries file-id valid-color?]
(let [resolved-file-id (if (= selected :file) file-id selected)]
(reset! open-groups* #{})
(if (= selected :recent)
(let [colors (into []
(comp
(filter valid-color?)
(map-indexed (fn [index color]
(let [color (if (map? color) color {:color color})]
(vary-meta color assoc ::id (dm/str index)))))
(take-while some?))
(sort ctc/sort-colors (reverse recent-colors)))]
(reset! current-colors* colors)
(reset! grouped-colors* {}))
resolved-file-id
(if (= selected :file) file-id selected)
(let [raw-colors (->> (dm/get-in libraries [resolved-file-id :data :colors])
(vals)
(filter valid-color?)
(sort-by :name))
;; Colors of the selected library; stable across unrelated commits
library-colors
(when (not= selected :recent)
(dm/get-in libraries [resolved-file-id :data :colors]))
;; Eager vector for the grid view -- index-based ::id for keying.
flat-colors (into []
(map-indexed (fn [index color]
(-> (ctc/library-color->color color resolved-file-id)
(vary-meta assoc ::id (dm/str index)))))
raw-colors)
;; Flat (grid and recent list) and grouped (library list) colors, converted for rendering
[current-colors grouped-colors]
(mf/with-memo [selected recent-colors library-colors resolved-file-id valid-color?]
(if (= selected :recent)
[(into []
(comp
(filter valid-color?)
(map-indexed (fn [index color]
(let [color (if (map? color) color {:color color})]
(vary-meta color assoc ::id (dm/str index)))))
(take-while some?))
(sort ctc/sort-colors (reverse recent-colors)))
{}]
;; Group tree with colors already converted -- no conversions at render time.
grouped (some-> (grp/group-assets raw-colors false)
(convert-grouped-colors resolved-file-id))]
(reset! current-colors* flat-colors)
(reset! grouped-colors* (or grouped {}))))))
(let [raw-colors (->> (vals library-colors)
(filter valid-color?)
(sort-by :name))
;; Eager vector for the grid view -- index-based ::id for keying.
flat-colors (into []
(map-indexed (fn [index color]
(-> (ctc/library-color->color color resolved-file-id)
(vary-meta assoc ::id (dm/str index)))))
raw-colors)
;; Group tree with colors already converted -- no conversions at render time.
grouped (some-> (grp/group-assets raw-colors false)
(convert-grouped-colors resolved-file-id))]
[flat-colors (or grouped {})])))]
;; Expands all groups when the selected library changes
(mf/with-effect [selected]
(reset! open-groups* #{}))
[:div {:class (stl/css :libraries)}
[:div {:class (stl/css :select-wrapper)}

View File

@ -121,10 +121,15 @@
(reset! internal-color* color)
(on-change color))))]
;; Syncs with color changes made outside the ramp; the colors the ramp
;; emits come back with the same components and are skipped
(mf/use-effect
(mf/deps color)
(fn []
(reset! internal-color* (enrich-color-map color))))
(let [color (enrich-color-map color)]
(when (not= (select-keys color [:h :s :v :alpha])
(select-keys internal-color [:h :s :v :alpha]))
(reset! internal-color* color)))))
[:*
[:> value-saturation-selector*

View File

@ -100,6 +100,21 @@
(when (= (:type panel) :component-swap)
[:> component-menu* {:shapes (:shapes panel) :is-swap-opened true}]))
(defn- get-shapes-with-children
"Returns the shapes of `selected` together with all their descendants."
[objects selected]
(loop [queue (into #queue [] selected)
visited selected]
(if-let [id (peek queue)]
(let [shape (get objects id)
children (:shapes shape)]
(if (seq children)
(let [new-children (remove visited children)]
(recur (into (pop queue) new-children)
(into visited new-children)))
(recur (pop queue) visited)))
(sequence (keep (d/getf objects)) visited))))
(mf/defc design-menu*
{::mf/private true}
[{:keys [selected objects page-id file-id shapes]}]
@ -145,29 +160,16 @@
(->> (dm/get-in grid-edition [edition :selected])
(map #(dm/get-in objects [edition :layout-grid-cells %])))
shapes-with-children*
(mf/use-state nil)
;; Deferred, so the subtree walk runs in a background render
deferred-selected
(mf/use-deferred selected)
_ (mf/use-effect
(mf/deps selected objects shapes)
(fn []
(reset! shapes-with-children* nil)
(let [result
(loop [queue (into #queue [] selected)
visited selected]
(if-let [id (peek queue)]
(let [shape (get objects id)
children (:shapes shape)]
(if (seq children)
(let [new-children (remove visited children)]
(recur (into (pop queue) new-children)
(into visited new-children)))
(recur (pop queue) visited)))
(sequence (keep (d/getf objects)) visited)))]
(reset! shapes-with-children* result))))
deferred-objects
(mf/use-deferred objects)
shapes-with-children
(deref shapes-with-children*)
(mf/with-memo [deferred-selected deferred-objects]
(get-shapes-with-children deferred-objects deferred-selected))
total-selected
(count selected)]