Fix reactive islands client-side navigation and hydration

Three bugs prevented islands from working during SX wire navigation:

1. components_for_request() only bundled Component and Macro defs, not
   Island defs — client never received defisland definitions during
   navigation (components_for_page for initial HTML shell was correct).

2. hydrate-island used morph-children which can't transfer addEventListener
   event handlers from freshly rendered DOM to existing nodes. Changed to
   clear+append so reactive DOM with live signal subscriptions is inserted
   directly.

3. asyncRenderToDom (client-side async page eval) checked _component but
   not _island on ~-prefixed names — islands fell through to generic eval
   which failed. Now delegates to renderDomIsland.

4. setInterval_/setTimeout_ passed SX Lambda objects directly to native
   timers. JS coerced them to "[object Object]" and tried to eval as code,
   causing "missing ] after element list". Added _wrapSxFn to convert SX
   lambdas to JS functions before passing to timers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 15:18:45 +00:00
parent 9a0173419a
commit 189a0258d9
14 changed files with 971 additions and 1001 deletions

View File

@@ -473,7 +473,7 @@ def components_for_request(source: str = "",
from quart import request
from .jinja_bridge import _COMPONENT_ENV
from .deps import components_needed
from .types import Component, Macro
from .types import Component, Island, Macro
from .parser import serialize
# Determine which components the page needs
@@ -493,7 +493,19 @@ def components_for_request(source: str = "",
parts = []
for key, val in _COMPONENT_ENV.items():
if isinstance(val, Component):
if isinstance(val, Island):
comp_name = f"~{val.name}"
if needed is not None and comp_name not in needed and key not in needed:
continue
if comp_name in loaded or val.name in loaded:
continue
param_strs = ["&key"] + list(val.params)
if val.has_children:
param_strs.extend(["&rest", "children"])
params_sx = "(" + " ".join(param_strs) + ")"
body_sx = serialize(val.body, pretty=True)
parts.append(f"(defisland ~{val.name} {params_sx} {body_sx})")
elif isinstance(val, Component):
comp_name = f"~{val.name}"
# Skip if not needed for this page
if needed is not None and comp_name not in needed and key not in needed: