Fix DOM-preserving hydration: text node mismatch + conditional markers

Two issues with the initial hydration implementation:

1. Text node mismatch: SSR merges adjacent text into one node
   ("0 / 16") but client renders three separate children. When the
   cursor ran out, new nodes were created but dom-append was
   unconditionally skipped. Fix: only skip append when the child
   already has a parent (existing SSR node). New nodes (nil parent)
   get appended even during hydration.

2. Conditional markers: dispatch-render-form for if/when/cond in
   island scope was injecting comment markers during hydration,
   corrupting the DOM. Fix: skip the reactive conditional machinery
   during hydration — just evaluate and render the active branch
   normally, walking the cursor. Reactivity for conditionals
   activates after the first user-triggered re-render.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 14:27:37 +00:00
parent a2a4d17d53
commit ab50c4516e
3 changed files with 54 additions and 20 deletions

View File

@@ -375,7 +375,11 @@
(fn () (render-to-dom arg env new-ns))) (fn () (render-to-dom arg env new-ns)))
(spread? child) (spread? child)
nil nil
:else (when (not (hydrating?)) (dom-append el child))))) :else (when
(or
(not (hydrating?))
(nil? (dom-parent child)))
(dom-append el child)))))
(assoc state "i" (inc (get state "i")))))))) (assoc state "i" (inc (get state "i"))))))))
(dict "i" 0 "skip" false) (dict "i" 0 "skip" false)
args) args)
@@ -477,9 +481,18 @@
((args :as list) (env :as dict) (ns :as string)) ((args :as list) (env :as dict) (ns :as string))
(if (if
(hydrating?) (hydrating?)
(do (let
(for-each (fn (x) (render-to-dom x env ns)) args) ((frag (create-fragment)))
(create-fragment)) (for-each
(fn
(x)
(let
((result (render-to-dom x env ns)))
(when
(and (not (spread? result)) (nil? (dom-parent result)))
(dom-append frag result))))
args)
frag)
(let (let
((frag (create-fragment))) ((frag (create-fragment)))
(for-each (for-each
@@ -553,7 +566,7 @@
(cond (cond
(= name "if") (= name "if")
(if (if
(island-scope?) (and (island-scope?) (not (hydrating?)))
(let (let
((marker (create-comment "r-if")) ((marker (create-comment "r-if"))
(current-nodes (list)) (current-nodes (list))
@@ -617,7 +630,7 @@
(create-fragment))))) (create-fragment)))))
(= name "when") (= name "when")
(if (if
(island-scope?) (and (island-scope?) (not (hydrating?)))
(let (let
((marker (create-comment "r-when")) ((marker (create-comment "r-when"))
(current-nodes (list)) (current-nodes (list))
@@ -701,7 +714,7 @@
frag))) frag)))
(= name "cond") (= name "cond")
(if (if
(island-scope?) (and (island-scope?) (not (hydrating?)))
(let (let
((marker (create-comment "r-cond")) ((marker (create-comment "r-cond"))
(current-nodes (list)) (current-nodes (list))
@@ -1080,7 +1093,9 @@
(c) (c)
(let (let
((result (render-to-dom c env ns))) ((result (render-to-dom c env ns)))
(when (not (hydrating?)) (dom-append el result)))) (when
(or (not (hydrating?)) (nil? (dom-parent result)))
(dom-append el result))))
children) children)
(do (when (hydrating?) (hydrate-exit-element)) el))))) (do (when (hydrating?) (hydrate-exit-element)) el)))))
(define (define
@@ -1135,7 +1150,9 @@
(c) (c)
(let (let
((result (render-to-dom c env ns))) ((result (render-to-dom c env ns)))
(when (not (hydrating?)) (dom-append el result)))) (when
(or (not (hydrating?)) (nil? (dom-parent result)))
(dom-append el result))))
children) children)
(do (when (hydrating?) (hydrate-exit-element)) el))))) (do (when (hydrating?) (hydrate-exit-element)) el)))))
(define (define

File diff suppressed because one or more lines are too long

View File

@@ -375,7 +375,11 @@
(fn () (render-to-dom arg env new-ns))) (fn () (render-to-dom arg env new-ns)))
(spread? child) (spread? child)
nil nil
:else (when (not (hydrating?)) (dom-append el child))))) :else (when
(or
(not (hydrating?))
(nil? (dom-parent child)))
(dom-append el child)))))
(assoc state "i" (inc (get state "i")))))))) (assoc state "i" (inc (get state "i"))))))))
(dict "i" 0 "skip" false) (dict "i" 0 "skip" false)
args) args)
@@ -477,9 +481,18 @@
((args :as list) (env :as dict) (ns :as string)) ((args :as list) (env :as dict) (ns :as string))
(if (if
(hydrating?) (hydrating?)
(do (let
(for-each (fn (x) (render-to-dom x env ns)) args) ((frag (create-fragment)))
(create-fragment)) (for-each
(fn
(x)
(let
((result (render-to-dom x env ns)))
(when
(and (not (spread? result)) (nil? (dom-parent result)))
(dom-append frag result))))
args)
frag)
(let (let
((frag (create-fragment))) ((frag (create-fragment)))
(for-each (for-each
@@ -553,7 +566,7 @@
(cond (cond
(= name "if") (= name "if")
(if (if
(island-scope?) (and (island-scope?) (not (hydrating?)))
(let (let
((marker (create-comment "r-if")) ((marker (create-comment "r-if"))
(current-nodes (list)) (current-nodes (list))
@@ -617,7 +630,7 @@
(create-fragment))))) (create-fragment)))))
(= name "when") (= name "when")
(if (if
(island-scope?) (and (island-scope?) (not (hydrating?)))
(let (let
((marker (create-comment "r-when")) ((marker (create-comment "r-when"))
(current-nodes (list)) (current-nodes (list))
@@ -701,7 +714,7 @@
frag))) frag)))
(= name "cond") (= name "cond")
(if (if
(island-scope?) (and (island-scope?) (not (hydrating?)))
(let (let
((marker (create-comment "r-cond")) ((marker (create-comment "r-cond"))
(current-nodes (list)) (current-nodes (list))
@@ -1080,7 +1093,9 @@
(c) (c)
(let (let
((result (render-to-dom c env ns))) ((result (render-to-dom c env ns)))
(when (not (hydrating?)) (dom-append el result)))) (when
(or (not (hydrating?)) (nil? (dom-parent result)))
(dom-append el result))))
children) children)
(do (when (hydrating?) (hydrate-exit-element)) el))))) (do (when (hydrating?) (hydrate-exit-element)) el)))))
(define (define
@@ -1135,7 +1150,9 @@
(c) (c)
(let (let
((result (render-to-dom c env ns))) ((result (render-to-dom c env ns)))
(when (not (hydrating?)) (dom-append el result)))) (when
(or (not (hydrating?)) (nil? (dom-parent result)))
(dom-append el result))))
children) children)
(do (when (hydrating?) (hydrate-exit-element)) el))))) (do (when (hydrating?) (hydrate-exit-element)) el)))))
(define (define