---
title: Avoid Legacy Lifecycle Hooks (did-insert, will-destroy, did-update)
impact: HIGH
impactDescription: Prevents memory leaks and enforces modern patterns
tags: components, lifecycle, anti-pattern, modifiers, derived-data
---
## Avoid Legacy Lifecycle Hooks (did-insert, will-destroy, did-update)
**Never use `{{did-insert}}`, `{{will-destroy}}`, or `{{did-update}}` in new code.** These legacy helpers create coupling between templates and component lifecycle, making code harder to test and maintain. Modern Ember provides better alternatives through derived data and custom modifiers.
### Why These Are Problematic
1. **Memory Leaks**: Easy to forget cleanup, especially with `did-insert`
2. **Tight Coupling**: Mixes template concerns with JavaScript logic
3. **Poor Testability**: Lifecycle hooks are harder to unit test
4. **Not Composable**: Can't be easily shared across components
5. **Deprecated Pattern**: Not recommended in modern Ember
### Alternative 1: Use Derived Data
For computed values or reactive transformations, use getters and `@cached`:
**❌ Incorrect (did-update):**
```glimmer-js
// app/components/user-greeting.gjs
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
class UserGreeting extends Component {
@tracked displayName = '';
@action
updateDisplayName() {
// Runs on every render - inefficient and error-prone
this.displayName = `${this.args.user.firstName} ${this.args.user.lastName}`;
}
Hello,
{{this.displayName}}
}
```
**✅ Correct (derived data with getter):**
```glimmer-js
// app/components/user-greeting.gjs
import Component from '@glimmer/component';
class UserGreeting extends Component {
// Automatically reactive - updates when args change
get displayName() {
return `${this.args.user.firstName} ${this.args.user.lastName}`;
}
Hello,
{{this.displayName}}
}
```
**✅ Even better (use @cached for expensive computations):**
```glimmer-js
// app/components/user-stats.gjs
import Component from '@glimmer/component';
import { cached } from '@glimmer/tracking';
class UserStats extends Component {
@cached
get sortedPosts() {
// Expensive computation only runs when @posts changes
return [...this.args.posts].sort((a, b) => b.createdAt - a.createdAt);
}
@cached
get statistics() {
return {
total: this.args.posts.length,
published: this.args.posts.filter((p) => p.published).length,
drafts: this.args.posts.filter((p) => !p.published).length,
};
}
Total: {{this.statistics.total}}
Published: {{this.statistics.published}}
Drafts: {{this.statistics.drafts}}
{{#each this.sortedPosts as |post|}}
{{post.title}}
{{/each}}
}
```
### Alternative 2: Use Custom Modifiers
For DOM side effects, element setup, or cleanup, use custom modifiers:
**❌ Incorrect (did-insert + will-destroy):**
```glimmer-js
// app/components/chart.gjs
import Component from '@glimmer/component';
import { action } from '@ember/object';
class Chart extends Component {
chartInstance = null;
@action
setupChart(element) {
this.chartInstance = new Chart(element, this.args.config);
}
willDestroy() {
super.willDestroy();
// Easy to forget cleanup!
this.chartInstance?.destroy();
}
}
```
**✅ Correct (custom modifier with automatic cleanup):**
```javascript
// app/modifiers/chart.js
import { modifier } from 'ember-modifier';
import { registerDestructor } from '@ember/destroyable';
export default modifier((element, [config]) => {
// Setup
const chartInstance = new Chart(element, config);
// Cleanup happens automatically
registerDestructor(element, () => {
chartInstance.destroy();
});
});
```
```glimmer-js
// app/components/chart.gjs
import chart from '../modifiers/chart';
```
### Alternative 3: Use Resources for Lifecycle Management
For complex state management with automatic cleanup, use `ember-resources`:
**❌ Incorrect (did-insert for data fetching):**
```glimmer-js
// app/components/user-profile.gjs
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
class UserProfile extends Component {
@tracked userData = null;
@tracked loading = true;
controller = new AbortController();
@action
async loadUser() {
this.loading = true;
try {
const response = await fetch(`/api/users/${this.args.userId}`, {
signal: this.controller.signal,
});
this.userData = await response.json();
} finally {
this.loading = false;
}
}
willDestroy() {
super.willDestroy();
this.controller.abort(); // Easy to forget!
}