fix(framework): synchronous DOM flush for view transitions and client created components#222
Merged
mohamedmansour merged 2 commits intomainfrom Apr 13, 2026
Merged
Conversation
…-created components WebUIElement was missing synchronous flush behavior in two critical paths, causing stale DOM in view-transition snapshots and blank client-created components. ## Problem 1. **setInitialState()** — called by the router during SPA navigation — set observable properties through reactive setters that coalesce updates into a pending microtask. The DOM was not updated until the microtask drained, but view transitions (document.startViewTransition) snapshot the DOM synchronously. Result: the snapshot captured stale content. 2. **Client-created components** (no SSR, created via document.createElement) wired their template DOM via \() but never flushed the initial observable values into the bindings. The component rendered with empty/ default text until the first reactive change triggered an update. 3. **Complex property bindings** (:prop) on child custom elements set the child's property via the reactive setter, which queued a microtask on the child. The parent's flush completed but the child's DOM was still stale — breaking synchronous consistency for view-transition snapshots. ## Fix - **setInitialState()**: call \() after setting all properties. This synchronously drains the pending microtask queue so the DOM is current before the function returns. - **Post-wiring render**: after \() completes for client-created components, call \() to flush initial observable values into the freshly-wired template DOM. Uses \ (not \) to avoid the path-index build which is deferred to the first reactive change. - **ATTR_KIND_COMPLEX in \**: after setting a complex property on a child element, duck-type check for \ and call it. This ensures child for-loops re-render synchronously when the parent flushes. The typeof check is necessary because the target may be a third-party custom element that is not a WebUIElement. ## Tests - **complex-prop** fixture (5 tests): parent with @observable array passes :items to a child that renders a <for> loop. Verifies initial render, reactive updates, clearing, setInitialState propagation, and critically: synchronous propagation without microtask wait. - **client-wire** fixture (3 tests): client-created component with shadow DOM verifies initial observable values render immediately after appendChild, reactive updates work, and values are available synchronously. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tion or class' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes synchronous DOM consistency issues in WebUIElement that affected view-transition snapshots and initial rendering of client-created (non-SSR) components, and adds Playwright regression fixtures to verify the behavior.
Changes:
- Flush pending reactive updates at the end of
setInitialState()so router-driven state application updates the DOM synchronously. - For non-SSR mounts, immediately patch bindings after
$wire()via$updateInstance()so initial observable/attr values render without waiting for a later reactive change. - After complex (
:prop) property bindings, synchronously flush childWebUIElementpending updates (duck-typed via$flushUpdates) to keep nested renders consistent during synchronous snapshots.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| packages/webui-framework/src/element.ts | Adds synchronous flush points for router state, client-created initial render, and complex-prop child propagation. |
| packages/webui-framework/tests/fixtures/complex-prop/fixture.html | New fixture page for complex :prop propagation scenario. |
| packages/webui-framework/tests/fixtures/complex-prop/element.ts | Defines parent/child elements and compiled templates to exercise complex prop + <for> rendering. |
| packages/webui-framework/tests/fixtures/complex-prop/complex-prop.spec.ts | Regression tests for child <for> loop updates, including synchronous setInitialState() propagation. |
| packages/webui-framework/tests/fixtures/client-wire/fixture.html | New fixture page for client-created component mounting (no SSR). |
| packages/webui-framework/tests/fixtures/client-wire/element.ts | Defines a client-created component with initial observable values bound into shadow DOM. |
| packages/webui-framework/tests/fixtures/client-wire/client-wire.spec.ts | Regression tests ensuring client-created components render initial values synchronously and update reactively. |
janechu
approved these changes
Apr 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
WebUIElement was missing synchronous flush behavior in two critical paths, causing stale DOM in view-transition snapshots and blank client-created components.
Problem
setInitialState() called by the router during SPA navigation, set observable properties through reactive setters that coalesce updates into a pending microtask. The DOM was not updated until the microtask drained, but view transitions (document.startViewTransition) snapshot the DOM synchronously. Result: the snapshot captured stale content.
Client-created components (no SSR, created via document.createElement) wired their template DOM but never flushed the initial observable values into the bindings. The component rendered with empty default text until the first reactive change triggered an update.
Complex property bindings (:prop) on child custom elements set the child's property via the reactive setter, which queued a microtask on the child. The parent's flush completed but the child's DOM was still stale, breaking synchronous consistency for view-transition snapshots.
Fix
setInitialState(): call after setting all properties. This synchronously drains the pending microtask queue so the DOM is current before the function returns.
Post-wiring render: after completes for client-created components, to flush initial observable values into the freshly-wired template DOM. Uses \ (not ) to avoid the path-index build which is deferred to the first reactive change.
**ATTR_KIND_COMPLEX in **: after setting a complex property on a child element, duck-type check for \ and call it. This ensures child for-loops re-render synchronously when the parent flushes. The typeof check is necessary because the target may be a third-party custom element that is not a WebUIElement.
Tests
complex-prop fixture (5 tests): parent with @observable array passes :items to a child that renders a loop. Verifies initial render, reactive updates, clearing, setInitialState propagation, and critically: synchronous propagation without microtask wait.
client-wire fixture (3 tests): client-created component with shadow DOM verifies initial observable values render immediately after appendChild, reactive updates work, and values are available synchronously.