Component Lifecycle
When each callback fires, what's available at each stage, and the one rule you must never break.
Lifecycle Overview
ShadowComponent follows the standard Web Components lifecycle, with a few things handled for you in the base class constructor:
constructor()
The constructor fires when the element is created (either by the parser or by document.createElement()). ShadowComponent's constructor does the heavy lifting:
- Calls
super()(required for all HTMLElement subclasses) - Creates getter/setter pairs for each entry in
static observables - Attaches the shadow root with
attachShadow({mode: "open"}) - Finds
template#tag-namein the document and clones its content into the shadow root
The element is not in the DOM during the constructor. You cannot read attributes that depend on document context, measure dimensions, or access parentElement. Save that work for connectedCallback().
If your subclass needs constructor logic, always call super() first:
class MyComponent extends ShadowComponent {
static observables = ["count"];
constructor() {
super(); // MUST be first — sets up shadow root, template, observables
this._internalState = new Map();
}
}
connectedCallback()
Fires when the element is inserted into the DOM. This is where you do most of your setup — querying shadow DOM elements, adding event listeners, fetching data, measuring dimensions.
super.connectedCallback() must be the first line in your connectedCallback(). The base class performs essential setup. Omitting this call will break your component.
connectedCallback() {
super.connectedCallback(); // ALWAYS first
// Now safe to do everything:
this.button = this.shadowRoot.querySelector(".my-button");
this.button.addEventListener("click", () => this.handleClick());
// Access attributes
const mode = this.getAttribute("mode") || "default";
// Measure things
const width = this.getBoundingClientRect().width;
}
What's Available
| Capability | constructor() | connectedCallback() |
|---|---|---|
this.shadowRoot |
Yes (after super()) |
Yes |
| Template content cloned | Yes (after super()) |
Yes |
| Observable getters/setters | Yes (after super()) |
Yes |
this.parentElement |
No | Yes |
this.getAttribute() |
Unreliable | Yes |
| DOM measurement | No | Yes |
| Event listeners on self | No | Yes |
| Slotted content accessible | No | Yes |
disconnectedCallback()
Fires when the element is removed from the DOM. Use it to clean up event listeners, cancel timers, or release resources. This method can fire multiple times if an element is added and removed repeatedly.
disconnectedCallback() {
// Clean up to prevent memory leaks
if (this._observer) {
this._observer.disconnect();
this._observer = null;
}
if (this._timer) {
clearInterval(this._timer);
this._timer = null;
}
// Remove document-level listeners
document.removeEventListener("keydown", this._boundKeyHandler);
}
attributeChangedCallback()
Fires whenever an observed attribute changes. ShadowComponent wires observedAttributes to return the static observables array, so any property you declare as observable triggers this callback when its corresponding attribute is set.
attributeChangedCallback(name, oldValue, newValue) {
// The base class handles slot synchronization automatically.
// Override only if you need additional side effects:
if (name === "count" && this.shadowRoot) {
this.shadowRoot.querySelector(".display").textContent = newValue;
}
}
The base class attributeChangedCallback does the following:
- Skips if
oldValue === newValue(no-op guard) - Parses the new value (handles JSON arrays and objects)
- Finds existing slot elements for the attribute name
- If counts match and values are primitives, patches in place (non-destructive)
- Otherwise, removes and recreates the slot elements
ShadowComponent does not have an afterRender() lifecycle hook. If you need to run code after the template is attached, do it at the end of connectedCallback(). If you need to react to attribute changes, use attributeChangedCallback().
Lifecycle Flow Diagram
shadow root + template + observables
setup, listeners, queries
slot sync + side effects
cleanup
Complete Example
<template id="live-clock">
<style>
:host { display: inline-block; font-family: var(--font-mono); }
.time { font-size: 1.2em; color: var(--accent-primary); }
</style>
<span class="time"></span>
</template>
<script>
class LiveClock extends ShadowComponent {
static observables = [];
connectedCallback() {
super.connectedCallback();
this._display = this.shadowRoot.querySelector(".time");
this._tick();
this._timer = setInterval(() => this._tick(), 1000);
}
disconnectedCallback() {
clearInterval(this._timer);
}
_tick() {
this._display.textContent = new Date().toLocaleTimeString();
}
}
customElements.define("live-clock", LiveClock);
</script>