Life-Cycle Hooks
Define behavior when certain events in the component’s life cycle is triggered by providing hook methods
See it live: Lifecycle order demo ↗ logs each hook as it fires, and Attribute lifecycle demo ↗ shows how attribute changes drive them.
onInit()
Section titled “onInit()”- Triggered when the component is connected to the DOM
- Best for setting up the component
import { WebComponent } from 'https://esm.sh/web-component-base@latest'
class ClickableText extends WebComponent { // gets called when the component is used in an HTML document onInit() { this.onclick = () => console.log('>>> click!') }
get template() { return `<span style="cursor:pointer">Click me!</span>` }}afterViewInit()
Section titled “afterViewInit()”- Triggered after the view is first initialized
class ClickableText extends WebComponent { // gets called when the component's innerHTML is first filled afterViewInit() { const footer = this.querySelector('footer') // do stuff to footer after view is initialized }
get template() { return `<footer>Awesome site © 2023</footer>` }}onDestroy()
Section titled “onDestroy()”- Triggered when the component is disconnected from the DOM
- best for undoing any setup done in
onInit()
import { WebComponent } from 'https://esm.sh/web-component-base@latest'
class ClickableText extends WebComponent { clickCallback() { console.log('>>> click!') }
onInit() { this.onclick = this.clickCallback }
onDestroy() { console.log('>>> removing event listener') this.removeEventListener('click', this.clickCallback) }
get template() { return `<span style="cursor:pointer">Click me!</span>` }}onChanges()
Section titled “onChanges()”- Triggered when an attribute value changed
- The
changesobject cleanly separates the property from the attribute:property: the camelCase prop key, matching how you accessprops(e.g.myName)attribute: the kebab-case attribute name that changed (e.g.my-name)previousValue/currentValue: the values before and after the change
Use property to read the value straight off props (this.props[property]); use attribute when you need the raw attribute name.
See it live: onChanges payload demo ↗
import { WebComponent } from 'https://esm.sh/web-component-base@latest'
class ClickableText extends WebComponent { // gets called when an attribute value changes onChanges(changes) { const { property, attribute, previousValue, currentValue } = changes console.log('>>> ', { property, attribute, previousValue, currentValue }) }
get template() { return `<span style="cursor:pointer">Click me!</span>` }}Upgrade ordering & the buffering guarantee
Section titled “Upgrade ordering & the buffering guarantee”Per the Custom Elements spec, when an element is upgraded with attributes already present in the markup (e.g. <my-el my-name="Zoe">), the browser fires attributeChangedCallback before connectedCallback. Taken literally, that means render() and onChanges() could run before onInit(), so any setup you do in onInit (event wiring, reading external state) would not have happened yet on that first render. Test environments like happy-dom/jsdom don’t reproduce this ordering, so components can pass in tests and then misbehave in a real browser.
WebComponent removes this footgun. Attribute changes that arrive before the element is connected are buffered:
- the prop value is applied immediately, so
this.propsis already correct insideonInit(); - the
render()andonChanges()side effects are deferred until afteronInit()runs.
On connect, the order is always:
onInit():this.propsalready reflects any authored attributes- a single
render(): reflects all buffered props in one pass afterViewInit()
onChanges() never fires before onInit(). Pre-connect attribute changes are not replayed through onChanges(). The first render() already reflects them, so onChanges() is reserved for genuine post-connect changes. After the element is connected, attribute changes behave normally: each one triggers render() and onChanges() immediately.