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

5.3 KiB

title, impact, impactDescription, tags
title impact impactDescription tags
Avoid Constructors in Components HIGH Prevents infinite render loops and simplifies code components, constructors, initialization, anti-pattern

Avoid Constructors in Components

Strongly discourage constructor usage. Modern Ember components rarely need constructors. Use class fields, @service decorators, and getPromiseState for initialization instead. Constructors with function calls that set tracked state can cause infinite render loops.

Incorrect (using constructor):

// app/components/user-profile.gjs
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { service } from '@ember/service';

class UserProfile extends Component {
  constructor() {
    super(...arguments);

    // Anti-pattern: Manual service lookup
    this.store = this.owner.lookup('service:store');
    this.router = this.owner.lookup('service:router');

    // Anti-pattern: Imperative initialization
    this.data = null;
    this.isLoading = false;
    this.error = null;

    // Anti-pattern: Side effects in constructor
    this.loadUserData();
  }

  async loadUserData() {
    this.isLoading = true;
    try {
      this.data = await this.store.request({
        url: `/users/${this.args.userId}`,
      });
    } catch (e) {
      this.error = e;
    } finally {
      this.isLoading = false;
    }
  }

  <template>
    {{#if this.isLoading}}
      <div>Loading...</div>
    {{else if this.error}}
      <div>Error: {{this.error.message}}</div>
    {{else if this.data}}
      <h1>{{this.data.name}}</h1>
    {{/if}}
  </template>
}

Correct (use class fields and declarative async state):

// app/components/user-profile.gjs
import Component from '@glimmer/component';
import { cached } from '@glimmer/tracking';
import { service } from '@ember/service';
import { getRequestState } from '@warp-drive/ember';

class UserProfile extends Component {
  @service store;

  @cached
  get userRequest() {
    return this.store.request({
      url: `/users/${this.args.userId}`,
    });
  }

  <template>
    {{#let (getRequestState this.userRequest) as |state|}}
      {{#if state.isPending}}
        <div>Loading...</div>
      {{else if state.isError}}
        <div>Error loading user</div>
      {{else}}
        <h1>{{state.value.name}}</h1>
      {{/if}}
    {{/let}}
  </template>
}

When You Might Need a Constructor (Very Rare):

Very rarely, you might need a constructor for truly exceptional cases. Even then, use modern patterns:

// app/components/complex-setup.gjs
import Component from '@glimmer/component';
import { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';

class ComplexSetup extends Component {
  @service store;

  @tracked state = null;

  constructor(owner, args) {
    super(owner, args);

    // Only if you absolutely must do something that can't be done with class fields
    // Even then, prefer resources or modifiers
    if (this.args.legacyInitMode) {
      this.initializeLegacyMode();
    }
  }

  initializeLegacyMode() {
    // Rare edge case initialization
  }

  <template>
    <!-- template -->
  </template>
}

Why Strongly Avoid Constructors:

  1. Infinite Render Loops: Setting tracked state in constructor that's read during render causes infinite loops
  2. Service Injection: Use @service decorator instead of owner.lookup()
  3. Testability: Class fields are easier to mock and test
  4. Clarity: Declarative class fields show state at a glance
  5. Side Effects: getPromiseState and modifiers handle side effects better
  6. Memory Leaks: getPromiseState auto-cleanup; constructor code doesn't
  7. Reactivity: Class fields integrate better with tracking
  8. Initialization Order: No need to worry about super() call timing
  9. Argument Validation: Constructor validation runs only once; use getters to catch arg changes

Modern Alternatives:

Old Pattern Modern Alternative
constructor() { this.store = owner.lookup('service:store') } @service store;
constructor() { this.data = null; } @tracked data = null;
constructor() { this.loadData(); } Use @cached get with getPromiseState
constructor() { this.interval = setInterval(...) } Use modifier with registerDestructor
constructor() { this.subscription = ... } Use modifier or constructor with registerDestructor ONLY

Performance Impact:

  • Before: Constructor runs on every instantiation, manual cleanup risk, infinite loop danger
  • After: Class fields initialize efficiently, getPromiseState auto-cleanup, no render loops

Strongly discourage constructors - they add complexity and infinite render loop risks. Use declarative class fields and getPromiseState instead.

Reference: