Files
marco/.agents/skills/ember-best-practices/rules/component-avoid-lifecycle-hooks.md

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

  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):

// 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:

  1. Identify the purpose: What is the hook doing?
  2. Choose the right alternative:
    • Deriving data? → Use getters/@cached
    • DOM setup/teardown? → Use a custom modifier
    • Async data loading? → Use getPromiseState from warp-drive
  3. Test thoroughly: Ensure cleanup happens correctly
  4. 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 @cached for 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: