fix: preserve parent state across delayed child upgrades - #438
Merged
Mohamed Mansour (mohamedmansour) merged 1 commit intoAug 13, 2026
Conversation
Use a weak WebUI-only handoff for complex SSR properties so delayed children receive parent state without accessor shadowing or retained definition promises. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 79ab856c-34d8-40c0-9d0d-7d21125739bb
Mohamed Mansour (mohamedmansour)
requested review from
a team,
Bang Lee (Qusic),
Akrosh Gandhi (akroshg) and
mcritzjam
and
a lite review from Copilot
August 13, 2026 03:03
Copilot started reviewing on behalf of
Mohamed Mansour (mohamedmansour)
August 13, 2026 04:30
View session
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes an SSR hydration edge case where complex :-bound values (arrays/objects) assigned from a parent to a child before the child’s custom element upgrades can accidentally create shadowing own-properties, preventing the child’s @observable accessors from receiving the intended value. The implementation preserves parent-provided state for unupgraded compiled WebUI children via a module-local WeakMap and applies it at the correct point in the child lifecycle.
Changes:
- Prime known complex
:properties during the existing SSR attribute-binding pass, and route all complex-property writes through a unified writer that is accessor-safe for unupgraded compiled WebUI children. - Queue pending parent-supplied complex values (and post-SSR updates that must replay) for unupgraded compiled WebUI children, then apply them after bootstrap state but before the child’s first binding walk.
- Expand fixture and unit/Playwright coverage to validate parent-first/child-first definition order, renamed parent state keys, delayed definitions, detached-yet-defined scenarios, and post-SSR parent updates.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| packages/webui-framework/src/template-element.ts | Adds pending-parent-state queueing/apply path and primes known complex : bindings during SSR hydration without introducing retained-element promises. |
| packages/webui-framework/src/template-element.test.ts | Adds unit tests ensuring queued complex properties do not create shadowing own-properties and preserves direct assignment for third-party elements. |
| packages/webui-framework/tests/fixtures/complex-prop/complex-prop.spec.ts | Adds Playwright regression coverage for definition-order permutations and delayed custom-element upgrade behavior. |
| packages/webui-framework/tests/fixtures/complex-prop/element.ts | Adjusts fixture element definitions to simulate parent-first hydration, detached-defined scenarios, and delayed child definition. |
| packages/webui-framework/tests/fixtures/complex-prop/src/test-item-host/test-item-host.html | Updates complex binding to use renamed parent property and adds the delayed child instance. |
| packages/webui-framework/tests/fixtures/complex-prop/src/test-delayed-prop-child/test-delayed-prop-child.html | Introduces delayed-child template that renders a <for> loop over a complex items array. |
| packages/webui-framework/tests/fixtures/complex-prop/state.json | Updates fixture bootstrap state keys (sourceItems, delayedItems) to exercise renamed and delayed paths. |
| packages/webui-framework/RENDERING.md | Documents the SSR attribute pass priming of known complex properties and weak pending-state handoff behavior. |
| DESIGN.md | Specifies the contract for complex : property hydration, unupgraded child handling, replay behavior, and third-party semantics. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
mcritzjam
approved these changes
Aug 13, 2026
Mohamed Mansour (mohamedmansour)
merged commit Aug 13, 2026
213b4a4
into
microsoft:main
10 checks passed
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.
Why this is needed
Complex
:bindings pass structured JavaScript values such as arrays and objects from a parent template into a child component. Unlike text and scalar attributes, those values do not have a durable HTML representation.That creates a definition-order gap during SSR hydration:
@observableaccessor on the class prototype. The reactive setter never receives the parent value, and a field initializer can replace it with the default.This is especially visible when the parent and child use different state names, for example
:items="{{sourceItems}}". Page-wide bootstrap state cannot seed the child'sitemsfield from the parent'ssourceItemskey, even though the SSR DOM was rendered correctly.The result is a component whose rendered SSR content and JavaScript state disagree. Repeat and conditional bindings can retain stale state, future writes can bypass the accessor, and component behavior depends on whether the parent or child happened to be defined first.
What changed
WeakMap<Element, PendingParentState>.DESIGN.mdand the framework maintainer rendering notes. This is a transparent runtime fix, so no user-facing guide changes are included.Why this design
The first version considered for this fix stored three
Symbol.for(...)values on every pending child and registered onecustomElements.whenDefined()continuation per element. That approach was not suitable for the framework runtime:Symbol.for(...)created a cross-runtime global protocol for private state;customElements.get()alone does not make direct assignment accessor-safe;The weak handoff keeps the normal upgraded-child path unchanged and allocation-free. Only a child that actually receives parent state before upgrade allocates one weak entry and one null-prototype value record. The replay
Setis allocated only when a newer parent value must be reconciled after SSR wiring. There are no definition promises, retained-element sets, per-tag registries, or global pending-state bridges.Regression coverage
The framework tests now cover:
Performance and memory
The baseline below is the initial per-element
whenDefined()pending-state implementation evaluated for this fix. The result is the weak handoff in this PR.Hydration timings are medians from 7 serial Playwright runs. Bundle size is a minified browser ESM build with
__WEBUI_DEV__=false. Retention was measured with a 2,000-element Chromium CDP stress probe followed by forced garbage collection.The timing results show no hydration regression; the deterministic bundle and retention results confirm the lower code and memory cost.
Validation
@microsoft/webui-frameworkunit and Playwright suitescargo xtask check