Constructor and connectedCallback
The constructor: internal state only
Section titled “The constructor: internal state only”The constructor runs when the element is instantiated — either via new MyElement() or when the browser parses the HTML and creates a class instance. At this point:
- The element may not yet be attached to any document
- Attributes are not yet parsed —
this.getAttribute()returnsnull - Children are not yet present — the element’s children haven’t been parsed yet
- Setting
innerHTML,textContent, or appending child nodes here will throw or produce unexpected results
The constructor’s job is narrow: call super(), set up internal JavaScript state (instance variables, default values, private Maps), and optionally attach a shadow root. Nothing more.
class MyWidget extends HTMLElement { constructor() { super(); // MUST be first — gives you access to `this` // Good: set up internal state this._count = 0; this._listeners = []; // Good: attach shadow root this.attachShadow({ mode: 'open' }); // BAD — do NOT do this in constructor: // this.innerHTML = '<p>hello</p>'; // may throw // this.getAttribute('label'); // always null here }}connectedCallback: rendering and listeners
Section titled “connectedCallback: rendering and listeners”connectedCallback fires each time the element is inserted into a document (or shadow root). By this point:
- The element is in the DOM
- Attributes set in HTML markup have been parsed and are readable via
getAttribute - It is safe to set
innerHTML, manipulate children, and attach event listeners
connectedCallback can fire more than once. If you move an element from one parent to another using appendChild, it is first disconnected (triggering disconnectedCallback) and then reconnected (triggering connectedCallback again). Guard against double-initialization if needed.
class MyWidget extends HTMLElement { constructor() { super(); this._initialized = false; }
connectedCallback() { if (this._initialized) return; // guard against re-connection this._initialized = true; var label = this.getAttribute('label') || 'default'; this.innerHTML = '<span>' + label + '</span>'; this.addEventListener('click', this._handleClick.bind(this)); }}Wrong vs. right
Section titled “Wrong vs. right”// WRONG — DOM manipulation in constructorclass BadWidget extends HTMLElement { constructor() { super(); this.innerHTML = '<p>' + this.getAttribute('title') + '</p>'; // title is null here! }}
// RIGHT — DOM manipulation in connectedCallbackclass GoodWidget extends HTMLElement { constructor() { super(); // only internal state this._title = ''; } connectedCallback() { this._title = this.getAttribute('title') || 'Untitled'; this.innerHTML = '<p>' + this._title + '</p>'; // attribute is available now }}Live demo
Section titled “Live demo”The element below appends a timestamped entry to the log for both constructor and connectedCallback. Use the buttons to remove and re-add the element and observe that connectedCallback fires each time while constructor fires only once.
Notice that constructor appears in the log only once — at element creation — while connectedCallback appears every time the element is re-inserted into the DOM.
Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
Set up state in constructor | Runs the earliest, before the element enters the DOM — good for pure internal state | No access to attributes, children, or parent at all, since the element isn’t inserted into the document yet |
Set up state in connectedCallback | Full access to attributes, light DOM children, and parent, and safe to render | Can be called more than once over the element’s lifetime — your code has to tolerate being re-run (idempotent) |
Common mistakes
Section titled “Common mistakes”- Reading an attribute in the
constructor— attributes aren’t set yet when the constructor runs; move that logic intoconnectedCallbackinstead. - Touching children or the parent in the
constructor— light DOM children aren’t fully parsed at that point, sothis.childrenorthis.parentNodemay be incomplete ornull. - Assuming
connectedCallbackonly fires once — moving an element withappendChildto a new parent makes the browser calldisconnectedCallbackand thenconnectedCallbackagain. Write it so a re-run doesn’t break or duplicate work (e.g. guard against adding the same listener twice).
💡 Real-world examples
Stencil (the compiler behind Ionic Framework’s components) documents this exact split in its lifecycle guide: keep the constructor to internal state, and put logic that depends on attributes/DOM into
connectedCallbackorcomponentWillLoadso components survive being re-mounted in an SPA. (stenciljs.com/docs/component-lifecycle)