8.9 KiB
title, impact, impactDescription, tags
| title | impact | impactDescription | tags |
|---|---|---|---|
| Avoid Legacy Lifecycle Hooks (did-insert, will-destroy, did-update) | HIGH | Prevents memory leaks and enforces modern patterns | 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
- Memory Leaks: Easy to forget cleanup, especially with
did-insert - Tight Coupling: Mixes template concerns with JavaScript logic
- Poor Testability: Lifecycle hooks are harder to unit test
- Not Composable: Can't be easily shared across components
- 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):
// 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}`;
}
<template>
<div {{did-update this.updateDisplayName @user}}>
Hello,
{{this.displayName}}
</div>
</template>
}
✅ Correct (derived data with getter):
// 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}`;
}
<template>
<div>
Hello,
{{this.displayName}}
</div>
</template>
}
✅ Even better (use @cached for expensive computations):
// 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,
};
}
<template>
<div>
<p>Total: {{this.statistics.total}}</p>
<p>Published: {{this.statistics.published}}</p>
<p>Drafts: {{this.statistics.drafts}}</p>
<ul>
{{#each this.sortedPosts as |post|}}
<li>{{post.title}}</li>
{{/each}}
</ul>
</div>
</template>
}
Alternative 2: Use Custom Modifiers
For DOM side effects, element setup, or cleanup, use custom modifiers:
❌ Incorrect (did-insert + will-destroy):
// 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();
}
<template>
<canvas {{did-insert this.setupChart}}></canvas>
</template>
}
✅ Correct (custom modifier with automatic cleanup):
// 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();
});
});
// app/components/chart.gjs
import chart from '../modifiers/chart';
<template>
<canvas {{chart @config}}></canvas>
</template>
Alternative 3: Use Resources for Lifecycle Management
For complex state management with automatic cleanup, use ember-resources:
❌ Incorrect (did-insert for data fetching):
// 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!
}
<template>
<div {{did-insert this.loadUser}}>
{{#if this.loading}}
Loading...
{{else}}
{{this.userData.name}}
{{/if}}
</div>
</template>
}
✅ Correct (Resource with automatic cleanup):
// app/resources/user-data.js
import { Resource } from 'ember-resources';
import { tracked } from '@glimmer/tracking';
export default class UserDataResource extends Resource {
@tracked data = null;
@tracked loading = true;
controller = new AbortController();
modify(positional, named) {
const [userId] = positional;
this.loadUser(userId);
}
async loadUser(userId) {
this.loading = true;
try {
const response = await fetch(`/api/users/${userId}`, {
signal: this.controller.signal,
});
this.data = await response.json();
} finally {
this.loading = false;
}
}
willDestroy() {
// Cleanup happens automatically
this.controller.abort();
}
}
// app/components/user-profile.gjs
import Component from '@glimmer/component';
import UserDataResource from '../resources/user-data';
class UserProfile extends Component {
userData = UserDataResource.from(this, () => [this.args.userId]);
<template>
{{#if this.userData.loading}}
Loading...
{{else}}
{{this.userData.data.name}}
{{/if}}
</template>
}
When to Use Each Alternative
| Use Case | Solution | Why |
|---|---|---|
| Computed values | Getters + @cached |
Reactive, efficient, no lifecycle needed |
| DOM manipulation | Custom modifiers | Encapsulated, reusable, automatic cleanup |
| Data fetching | getPromiseState from warp-drive | Declarative, automatic cleanup |
| Event listeners | {{on}} modifier |
Built-in, automatic cleanup |
| Focus management | Custom modifier or ember-focus-trap | Proper lifecycle, accessibility |
Migration Strategy
If you have existing code using these hooks:
- Identify the purpose: What is the hook doing?
- Choose the right alternative:
- Deriving data? → Use getters/
@cached - DOM setup/teardown? → Use a custom modifier
- Async data loading? → Use getPromiseState from warp-drive
- Deriving data? → Use getters/
- Test thoroughly: Ensure cleanup happens correctly
- Remove the legacy hook: Delete
{{did-insert}},{{will-destroy}}, or{{did-update}}
Performance Benefits
Modern alternatives provide better performance:
- Getters: Only compute when dependencies change
- @cached: Memoizes expensive computations
- Modifiers: Scoped to specific elements, composable
- getPromiseState: Declarative data loading, automatic cleanup
Common Pitfalls to Avoid
❌ Don't use willDestroy() for cleanup when a modifier would work
❌ Don't use @action + did-insert when a getter would suffice
❌ Don't manually track changes when @cached handles it automatically
❌ Don't forget registerDestructor in custom modifiers
Summary
Modern Ember provides superior alternatives to legacy lifecycle hooks:
- Derived Data: Use getters and
@cachedfor reactive computations - DOM Side Effects: Use custom modifiers with
registerDestructor - Async Data Loading: Use getPromiseState from warp-drive/reactiveweb
- Better Code: More testable, composable, and maintainable
Never use {{did-insert}}, {{will-destroy}}, or {{did-update}} in new code.
Reference: