Skip to content

fix: resolve SSR hydration ordinal drift from structural blocks#216

Merged
mohamedmansour merged 2 commits intomainfrom
mmansour/fix-hydration-ordinal-drift
Apr 10, 2026
Merged

fix: resolve SSR hydration ordinal drift from structural blocks#216
mohamedmansour merged 2 commits intomainfrom
mmansour/fix-hydration-ordinal-drift

Conversation

@mohamedmansour
Copy link
Copy Markdown
Contributor

Compiled template paths to SSR DOM nodes. When conditional () or repeat () blocks render content into the SSR DOM, the extra nodes shift ordinals so later siblings resolve to the wrong element.

Root cause: the compiled template static HTML (meta.h) strips structural blocks, but the SSR DOM contains them inline between marker pairs (... and ...). The ordinal counting did not account for these extra ranges.

This manifested in the commerce app when navigating from a zero-result search page (/search?q=test) to a category: the loop's parent container resolved to the

no-results message instead of

, leaving the product grid empty after SPA navigation.

Framework changes (webui-framework):

  • Extract findByOrdinal() into markers.ts — walks parent children counting only nodes outside structural block marker pairs, with depth tracking for nested blocks of the same type. Both (element ordinals) and (text ordinals) now delegate to this single function.

  • Defer SSR marker removal — closing markers (, ) and item markers () are collected during hydration but only removed after all path-based resolution is complete (after for events/refs). This ensures marker pairs are intact whenever findByOrdinal needs to skip structural blocks, regardless of hydration phase ordering.

  • Add 10 unit tests for findByOrdinal covering: conditional skipping, repeat skipping, nested conditionals, multiple sequential blocks, empty blocks, text ordinal drift, interleaved block types, and edge cases (empty parent, out-of-range ordinal).

Commerce app changes:

  • Fix mp-category-nav @attr mode — change all-active from string mode (default '') to boolean mode (default false). String mode produced '' (falsy) from the boolean attribute, causing a hydration mismatch where the server-rendered block was not tracked by the client, leaving orphaned 'All' DOM nodes visible after SPA navigation.

  • Fix Related Products scrollbar visibility — the scrollbar thumb used --webui-border (#262626), the same color as the track (--webui-surface #262626), making it invisible. Changed to --webui-text-muted and added scrollbar-width/scrollbar-color for Firefox cross-browser support.

  • Add regression test for search→category SPA navigation verifying category nav state and product grid population.

mohamedmansour and others added 2 commits April 10, 2026 14:39
 and  count element/text ordinals to map
compiled template paths to SSR DOM nodes.  When conditional (<if>) or
repeat (<for>) blocks render content into the SSR DOM, the extra
nodes shift ordinals so later siblings resolve to the wrong element.

Root cause: the compiled template static HTML (meta.h) strips
structural blocks, but the SSR DOM contains them inline between
marker pairs (<!--wc-->...<!--/wc--> and <!--wr-->...<!--/wr-->).
The ordinal counting did not account for these extra ranges.

This manifested in the commerce app when navigating from a zero-result
search page (/search?q=test) to a category: the <for> loop's parent
container resolved to the <p> no-results message instead of
<div class=grid>, leaving the product grid empty after SPA navigation.

Framework changes (webui-framework):

- Extract findByOrdinal() into markers.ts — walks parent children
  counting only nodes outside structural block marker pairs, with
  depth tracking for nested blocks of the same type.  Both
   (element ordinals) and  (text ordinals)
  now delegate to this single function.

- Defer SSR marker removal — closing markers (<!--/wc-->, <!--/wr-->)
  and item markers (<!--wi-->) are collected during hydration but
  only removed after all path-based resolution is complete (after
   for events/refs).  This ensures marker pairs are intact
  whenever findByOrdinal needs to skip structural blocks, regardless
  of hydration phase ordering.

- Add 10 unit tests for findByOrdinal covering: conditional skipping,
  repeat skipping, nested conditionals, multiple sequential blocks,
  empty blocks, text ordinal drift, interleaved block types, and
  edge cases (empty parent, out-of-range ordinal).

Commerce app changes:

- Fix mp-category-nav @attr mode — change all-active from string mode
  (default '') to boolean mode (default false).  String mode produced
  '' (falsy) from the boolean attribute, causing a hydration mismatch
  where the server-rendered <if condition=allActive> block was not
  tracked by the client, leaving orphaned 'All' DOM nodes visible
  after SPA navigation.

- Fix Related Products scrollbar visibility — the scrollbar thumb
  used --webui-border (#262626), the same color as the track
  (--webui-surface #262626), making it invisible.  Changed to
  --webui-text-muted and added scrollbar-width/scrollbar-color for
  Firefox cross-browser support.

- Add regression test for search→category SPA navigation verifying
  category nav state and product grid population.

- Update visual regression snapshots.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread examples/app/commerce/package.json
Comment thread packages/webui-framework/src/element.ts
@mohamedmansour mohamedmansour merged commit 3ff0c4f into main Apr 10, 2026
20 of 21 checks passed
@mohamedmansour mohamedmansour deleted the mmansour/fix-hydration-ordinal-drift branch April 10, 2026 22:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants