Ember Component Lifecycle — Constructor to Destruction
In this tutorial, you will learn about Ember Component Lifecycle. We cover key concepts, practical examples, and best practices to help you master this topic.
Ember components have a defined lifecycle from creation to destruction. The constructor initializes state, getters compute derived data, modifiers handle DOM updates, and willDestroy cleans up resources. Understanding the lifecycle prevents memory leaks and ensures predictable rendering.
What You'll Learn
You will learn the Glimmer component lifecycle stages, how to use modifiers for DOM access, how to respond to argument changes, and how to clean up resources properly.
Why It Matters
Incorrect lifecycle management causes memory leaks, stale data, and unexpected behavior. A component that fetches data on creation but never cancels the fetch keeps running after destruction.
Real-World Use
A real-time dashboard component subscribes to a Websocket in constructor, updates tracked properties on each message, and unsubscribes in willDestroy. This prevents WebSocket connections from leaking when users navigate away.
flowchart LR
A[Constructor] --> B[Getters computed]
B --> C[did-insert modifier]
C --> D[DOM available]
D --> E[did-update modifier]
E --> D
D --> F[willDestroy]
F --> G[Cleanup complete]
Lifecycle Stage 1: Constructor
The constructor runs when the component is created. Initialize tracked properties and set up non-DOM resources.
// app/components/timer.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export default class TimerComponent extends Component {
@tracked elapsed = 0;
@tracked isRunning = false;
constructor(owner, args) {
super(owner, args);
console.log('Timer component created');
this.startTime = Date.now();
this.isRunning = true;
}
}
Lifecycle Stage 2: Getters and Tracked Properties
Getters recompute whenever tracked properties they depend on change. They run before the first render and after every tracked change.
// app/components/price-display.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export default class PriceDisplayComponent extends Component {
@tracked taxRate = 0.1;
@tracked discount = 0;
get basePrice() {
return this.args.product?.price || 0;
}
get taxAmount() {
return this.basePrice * this.taxRate;
}
get discountAmount() {
return this.basePrice * (this.discount / 100);
}
get totalPrice() {
return this.basePrice + this.taxAmount - this.discountAmount;
}
get formattedTotal() {
return `$${this.totalPrice.toFixed(2)}`;
}
}
Lifecycle Stage 3: did-insert Modifier
The {{did-insert}} modifier runs after the component element is inserted into the DOM. Use it for DOM measurements, third-party library integration, and subscribing to external events.
// app/components/chart.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class ChartComponent extends Component {
@tracked chartInstance = null;
@action
initializeChart(element) {
console.log('Chart element mounted:', element);
// Third-party chart library integration
this.chartInstance = new ChartLib(element, {
type: this.args.type || 'bar',
data: this.args.data,
options: { responsive: true }
});
}
@action
updateChart(element) {
if (this.chartInstance) {
this.chartInstance.data = this.args.data;
this.chartInstance.update();
}
}
}
{{! app/components/chart.hbs }}
<div
class="chart-container"
{{did-insert this.initializeChart}}
{{did-update this.updateChart @data}}
>
<canvas></canvas>
</div>
Lifecycle Stage 4: did-update Modifier
The {{did-update}} modifier runs when specified arguments change. It is used to respond to data changes after initial render.
// app/components/search-results.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
export default class SearchResultsComponent extends Component {
@service store;
@tracked results = [];
@tracked isLoading = false;
@action
async loadResults(element, [query]) {
if (!query || query.length < 3) {
this.results = [];
return;
}
this.isLoading = true;
try {
this.results = await this.store.query('post', { q: query });
} catch (error) {
console.error('Search failed:', error);
this.results = [];
} finally {
this.isLoading = false;
}
}
}
{{! app/components/search-results.hbs }}
{{#if this.isLoading}}
<div class="spinner">Searching...</div>
{{else if this.results.length}}
<ul class="results" {{did-update this.loadResults @query}}>
{{#each this.results as |result|}}
<li>{{result.title}}</li>
{{/each}}
</ul>
{{else if @query}}
<p class="no-results">No results found for "{{@query}}"</p>
{{/if}}
Lifecycle Stage 5: willDestroy
The willDestroy method runs when the component is about to be removed. Clean up all resources: timers, subscriptions, DOM listeners, and third-party instances.
// app/components/live-feed.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class LiveFeedComponent extends Component {
@service session;
@tracked messages = [];
@tracked connectionStatus = 'disconnected';
constructor(owner, args) {
super(owner, args);
this.connect();
}
connect() {
this.connectionStatus = 'connecting';
this.ws = new WebSocket(this.args.url);
this.ws.onopen = () => {
this.connectionStatus = 'connected';
};
this.ws.onmessage = (event) => {
let data = JSON.parse(event.data);
this.messages = [...this.messages, data];
};
this.ws.onerror = () => {
this.connectionStatus = 'error';
};
}
willDestroy() {
console.log('Cleaning up WebSocket connection');
if (this.ws) {
this.ws.close();
this.ws = null;
}
super.willDestroy();
}
}
Modifier Lifecycle
Modifiers have their own lifecycle: did-insert, did-update, and will-destroy.
// app/modifiers/click-outside.js
import { modifier } from 'ember-modifier';
export default modifier((element, [handler]) => {
let onClickOutside = (event) => {
if (!element.contains(event.target)) {
handler(event);
}
};
document.addEventListener('click', onClickOutside);
// Cleanup function — runs on willDestroy
return () => {
document.removeEventListener('click', onClickOutside);
};
});
{{! Usage }}
<div {{click-outside this.closeDropdown}}>
Dropdown content
</div>
Complete Lifecycle Example
// app/components/lifecycle-demo.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class LifecycleDemoComponent extends Component {
@tracked mountTime = null;
constructor(owner, args) {
super(owner, args);
console.log('1. Constructor');
}
get processedArg() {
console.log('2. Getter running');
return this.args.value ? `Processed: ${this.args.value}` : 'No value';
}
@action
onInsert(element) {
this.mountTime = Date.now();
console.log('3. did-insert — DOM available');
console.log(' Element:', element.tagName);
}
@action
onUpdate(element, [value]) {
console.log('4. did-update — value changed to:', value);
}
willDestroy() {
console.log('5. willDestroy — cleaning up');
super.willDestroy();
}
}
Common Mistakes
- Not calling
super.willDestroy(). Failing to call super breaks parent cleanup. Always callsuper.willDestroy()in your implementation. - Setting up DOM listeners in constructor. The DOM does not exist in constructor. Use
did-insertmodifier for DOM access. - Modifying tracked properties synchronously in getters. Getters should not have side effects. They should only compute and return values.
- Not cleaning up third-party library instances. Chart instances, map instances, and other third-party objects must be destroyed in
willDestroy. - Using
did-updatewithout specifying dependent keys. Without dependents,did-updateruns on every render. Specify the exact arguments that trigger updates.
Practice Questions
- What lifecycle stage runs when a component is first inserted into the DOM?
- How do you react to changes in component arguments?
- What should you clean up in the
willDestroymethod? - Why should you not access the DOM in the constructor?
- Challenge: Create a component that fetches data from an API when inserted, tracks loading state, supports refetch when arguments change, and cancels the fetch on destruction. Use
AbortControllerfor cancellation.
FAQ
Mini Project
Create a DataTable component with full lifecycle management. Constructor: initialize default sort/filter state. did-insert: measure column widths, add resize Observer. did-update: re-sort when sort arg changes, re-filter when filter arg changes. willDestroy: disconnect observers, clean up sort/filter state. Each lifecycle stage should log its action.
What's Next
Now that you understand the lifecycle, learn Ember Helpers for template transformations. Then explore Ember Data for the data layer.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro