Before filing
Area
WebUI Framework package
Problem or need
connectedCallback is the only per-component lifecycle hook an author has, but it does not
fire at a fixed point relative to that component's hydration (the framework wiring its bindings
and events onto the SSR DOM). Depending on when the element upgrades during parsing, the
framework may defer hydration to the DOM's DOMContentLoaded / load event — so
connectedCallback sometimes runs before hydration and sometimes triggers it. From the
author's side this timing is non-deterministic and invisible.
There is no hook that runs after a component's own hydration completes. So the natural
thing — do setup in connectedCallback (write reactive state, mutate the DOM, read a wired
result) — races or precedes the hydration pass and is silently lost or corrupted. On SSR the
framework trusts the server DOM and does not reconcile, so the failure is silent and
SSR-only: it passes CSR, unit tests, and non-SSR local dev.
With no completion callback, the only way to "act after hydration" is for the author to
mirror the framework's internal deferral timing in their own code (by referencing its
source) — which is exactly what should be a supported lifecycle hook.
In one line: connectedCallback ≠ "hydrated," and there is no "hydrated" hook.
Impact
This one gap is the root cause behind several independently-reported cases. Each is an author
doing setup in connectedCallback / parse and the hydration pass silently discarding it. The
why differs per case and belongs in each linked report — not repeated here:
| Case |
Author does before hydration completes |
Silent result |
Details |
| DOM ↔ observable disagree |
sets an @observable |
DOM keeps the server value; state and DOM diverge permanently |
#379 |
| parent overwrites child |
child self-resolves its own value |
parent's later binding overwrites it |
#324 |
| self-removing element |
a marker element removes itself |
a sibling's @click never wires (dead control) |
attached webui-hydration-repro.zip |
They were each triaged individually. #379, notably, was answered with a dev-only
console.warn mismatch diagnostic — which reports that the author acted at the wrong time but
still gives them no right time to act at. Fixing these one-by-one is whack-a-mole against
one missing lifecycle primitive.
webui-hydration-repro.zip
Who would benefit?
Any team building interactive components on SSR. Common patterns that all need the same thing
— "run after my own hydration is done" — include: an element that removes itself after
recording a timing mark, a control that self-initializes from a browser/runtime API on
connect, lazy-loaded panels that fill after mount, and store-bound deferred setters. Today each
independently hand-rolls the same fragile private-timing workaround.
Desired outcome
An additive, per-component "hydration complete" hook, symmetric to connectedCallback:
- P0 — protected
hydratedCallback(): called once, after this component's bindings +
events are wired. The missing lifecycle method.
- P1 — queryable per-instance state:
get hydrated(): boolean + whenHydrated(): Promise<void>,
so external/late callers check state instead of racing an event.
- P2 — fix the existing global signal (
webui:hydration-complete) if it stays: it is
aggregate-only, one-shot/non-queryable, and fires early on the SSR-deferred path (a separate
defect worth its own note). A per-component hook is what actually closes the gap.
Concrete example
class MyComponent extends WebUIElement {
connectedCallback() {
super.connectedCallback();
// ❌ today: state/DOM touched here races the hydration pass → silently lost
}
hydratedCallback() {
// ✅ proposed: this component's bindings + events are wired; safe to act
}
}
// external caller:
await el.whenHydrated();
Constraints
- Additive only — the framework's hydration/wiring behavior is correct and must not
change; this asks for a hook, not a behavior change.
- Per-component and SSR-deferred-safe — the one existing global signal fires early exactly
on the SSR path where it matters, so an aggregate or CSR-only solution does not close the gap.
- Robust regardless of when the component hydrates — a component that hydrates after
DOMContentLoaded (async / dynamic-import bundle) must still receive the signal.
Alternatives or workarounds
Teams reference the framework's internal deferral and re-create it as a memoized
DOMContentLoaded promise, then gate their post-hydration work on it. It is duplicated across
teams, coupled to private timing, breaks when a component hydrates after DOMContentLoaded, and
is undiscoverable until a silent SSR bug ships. It is the symptom this feature removes.
Before filing
Area
WebUI Framework package
Problem or need
connectedCallbackis the only per-component lifecycle hook an author has, but it does notfire at a fixed point relative to that component's hydration (the framework wiring its bindings
and events onto the SSR DOM). Depending on when the element upgrades during parsing, the
framework may defer hydration to the DOM's
DOMContentLoaded/ load event — soconnectedCallbacksometimes runs before hydration and sometimes triggers it. From theauthor's side this timing is non-deterministic and invisible.
There is no hook that runs after a component's own hydration completes. So the natural
thing — do setup in
connectedCallback(write reactive state, mutate the DOM, read a wiredresult) — races or precedes the hydration pass and is silently lost or corrupted. On SSR the
framework trusts the server DOM and does not reconcile, so the failure is silent and
SSR-only: it passes CSR, unit tests, and non-SSR local dev.
With no completion callback, the only way to "act after hydration" is for the author to
mirror the framework's internal deferral timing in their own code (by referencing its
source) — which is exactly what should be a supported lifecycle hook.
In one line:
connectedCallback≠ "hydrated," and there is no "hydrated" hook.Impact
This one gap is the root cause behind several independently-reported cases. Each is an author
doing setup in
connectedCallback/ parse and the hydration pass silently discarding it. Thewhy differs per case and belongs in each linked report — not repeated here:
@observable@clicknever wires (dead control)webui-hydration-repro.zipThey were each triaged individually. #379, notably, was answered with a dev-only
console.warnmismatch diagnostic — which reports that the author acted at the wrong time butstill gives them no right time to act at. Fixing these one-by-one is whack-a-mole against
one missing lifecycle primitive.
webui-hydration-repro.zip
Who would benefit?
Any team building interactive components on SSR. Common patterns that all need the same thing
— "run after my own hydration is done" — include: an element that removes itself after
recording a timing mark, a control that self-initializes from a browser/runtime API on
connect, lazy-loaded panels that fill after mount, and store-bound deferred setters. Today each
independently hand-rolls the same fragile private-timing workaround.
Desired outcome
An additive, per-component "hydration complete" hook, symmetric to
connectedCallback:hydratedCallback(): called once, after this component's bindings +events are wired. The missing lifecycle method.
get hydrated(): boolean+whenHydrated(): Promise<void>,so external/late callers check state instead of racing an event.
webui:hydration-complete) if it stays: it isaggregate-only, one-shot/non-queryable, and fires early on the SSR-deferred path (a separate
defect worth its own note). A per-component hook is what actually closes the gap.
Concrete example
Constraints
change; this asks for a hook, not a behavior change.
on the SSR path where it matters, so an aggregate or CSR-only solution does not close the gap.
DOMContentLoaded(async / dynamic-import bundle) must still receive the signal.Alternatives or workarounds
Teams reference the framework's internal deferral and re-create it as a memoized
DOMContentLoadedpromise, then gate their post-hydration work on it. It is duplicated acrossteams, coupled to private timing, breaks when a component hydrates after
DOMContentLoaded, andis undiscoverable until a silent SSR bug ships. It is the symptom this feature removes.