Add keyed list reconciliation to reactive-list
Items with :key attributes are matched by key across renders — existing DOM nodes are reused, stale nodes removed, new nodes inserted in order. Falls back to clear-and-rerender without keys. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -580,29 +580,89 @@
|
||||
|
||||
;; reactive-list — render a keyed list bound to a signal
|
||||
;; Used for (map fn (deref items)) inside an island.
|
||||
;;
|
||||
;; Keyed reconciliation: if rendered elements have a "key" attribute,
|
||||
;; existing DOM nodes are reused across updates. Only additions, removals,
|
||||
;; and reorderings touch the DOM. Without keys, falls back to clear+rerender.
|
||||
|
||||
(define render-list-item
|
||||
(fn (map-fn item env ns)
|
||||
(if (lambda? map-fn)
|
||||
(render-lambda-dom map-fn (list item) env ns)
|
||||
(render-to-dom (apply map-fn (list item)) env ns))))
|
||||
|
||||
(define extract-key
|
||||
(fn (node index)
|
||||
;; Extract key from rendered node: :key attr, data-key, or index fallback
|
||||
(let ((k (dom-get-attr node "key")))
|
||||
(if k
|
||||
(do (dom-remove-attr node "key") k)
|
||||
(let ((dk (dom-get-data node "key")))
|
||||
(if dk (str dk) (str "__idx_" index)))))))
|
||||
|
||||
(define reactive-list
|
||||
(fn (map-fn items-sig env ns)
|
||||
(let ((container (create-fragment))
|
||||
(marker (create-comment "island-list")))
|
||||
(marker (create-comment "island-list"))
|
||||
(key-map (dict))
|
||||
(key-order (list)))
|
||||
(dom-append container marker)
|
||||
(effect (fn ()
|
||||
;; Simple strategy: clear and re-render
|
||||
;; Future: keyed reconciliation
|
||||
(let ((parent (dom-parent marker)))
|
||||
(let ((parent (dom-parent marker))
|
||||
(items (deref items-sig)))
|
||||
(when parent
|
||||
;; Remove all nodes after marker until next sibling marker
|
||||
(dom-remove-children-after marker)
|
||||
;; Render new items into a fragment, then insert after marker
|
||||
(let ((items (deref items-sig))
|
||||
(frag (create-fragment)))
|
||||
(for-each
|
||||
(fn (item)
|
||||
(let ((rendered (if (lambda? map-fn)
|
||||
(render-lambda-dom map-fn (list item) env ns)
|
||||
(render-to-dom (apply map-fn (list item)) env ns))))
|
||||
(dom-append frag rendered)))
|
||||
(let ((new-map (dict))
|
||||
(new-keys (list))
|
||||
(has-keys false))
|
||||
|
||||
;; Render or reuse each item
|
||||
(for-each-indexed
|
||||
(fn (idx item)
|
||||
(let ((rendered (render-list-item map-fn item env ns))
|
||||
(key (extract-key rendered idx)))
|
||||
(when (and (not has-keys)
|
||||
(not (starts-with? key "__idx_")))
|
||||
(set! has-keys true))
|
||||
;; Reuse existing node if key matches, else use new
|
||||
(if (dict-has? key-map key)
|
||||
(dict-set! new-map key (dict-get key-map key))
|
||||
(dict-set! new-map key rendered))
|
||||
(append! new-keys key)))
|
||||
items)
|
||||
(dom-insert-after marker frag))))))
|
||||
|
||||
(if (not has-keys)
|
||||
;; No keys: simple clear and re-render (original strategy)
|
||||
(do
|
||||
(dom-remove-children-after marker)
|
||||
(let ((frag (create-fragment)))
|
||||
(for-each
|
||||
(fn (k) (dom-append frag (dict-get new-map k)))
|
||||
new-keys)
|
||||
(dom-insert-after marker frag)))
|
||||
|
||||
;; Keyed reconciliation
|
||||
(do
|
||||
;; Remove stale nodes
|
||||
(for-each
|
||||
(fn (old-key)
|
||||
(when (not (dict-has? new-map old-key))
|
||||
(dom-remove (dict-get key-map old-key))))
|
||||
key-order)
|
||||
|
||||
;; Reorder/insert to match new key order
|
||||
(let ((cursor marker))
|
||||
(for-each
|
||||
(fn (k)
|
||||
(let ((node (dict-get new-map k))
|
||||
(next (dom-next-sibling cursor)))
|
||||
(when (not (identical? node next))
|
||||
(dom-insert-after cursor node))
|
||||
(set! cursor node)))
|
||||
new-keys))))
|
||||
|
||||
;; Update state for next render
|
||||
(set! key-map new-map)
|
||||
(set! key-order new-keys))))))
|
||||
container)))
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user