diff --git a/.agents/skills/ember-best-practices/AGENTS.md b/.agents/skills/ember-best-practices/AGENTS.md deleted file mode 100644 index ca70004afdd..00000000000 --- a/.agents/skills/ember-best-practices/AGENTS.md +++ /dev/null @@ -1,8579 +0,0 @@ -# Ember Best Practices - -**Version 1.0.0** -Ember.js Community -January 2026 - -> **Note:** -> This document is mainly for agents and LLMs to follow when maintaining, -> generating, or refactoring Ember.js codebases. Humans -> may also find it useful, but guidance here is optimized for automation -> and consistency by AI-assisted workflows. - ---- - -## Abstract - -Comprehensive performance optimization and accessibility guide for Ember.js applications, designed for AI agents and LLMs. Contains 42 rules across 7 categories, prioritized by impact from critical (route loading optimization, build performance) to advanced patterns (Resources, ember-concurrency, modern testing, composition patterns, owner/linkage management). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation. Uses WarpDrive for modern data management, includes accessibility best practices leveraging ember-a11y-testing and other OSS tools, and comprehensive coverage of reactive composition, data derivation, controlled forms, conditional rendering, data requesting patterns, built-in helpers, and service architecture patterns. - ---- - -## Table of Contents - -1. [Route Loading and Data Fetching](#1-route-loading-and-data-fetching) — **CRITICAL** - - 1.1 [Implement Smart Route Model Caching](#11-implement-smart-route-model-caching) - - 1.2 [Parallel Data Loading in Model Hooks](#12-parallel-data-loading-in-model-hooks) - - 1.3 [Use Loading Substates for Better UX](#13-use-loading-substates-for-better-ux) - - 1.4 [Use Route Templates with Co-located Syntax](#14-use-route-templates-with-co-located-syntax) - - 1.5 [Use Route-Based Code Splitting](#15-use-route-based-code-splitting) -2. [Build and Bundle Optimization](#2-build-and-bundle-optimization) — **CRITICAL** - - 2.1 [Avoid Importing Entire Addon Namespaces](#21-avoid-importing-entire-addon-namespaces) - - 2.2 [Lazy Load Heavy Dependencies](#22-lazy-load-heavy-dependencies) - - 2.3 [Use Embroider Build Pipeline](#23-use-embroider-build-pipeline) -3. [Component and Reactivity Optimization](#3-component-and-reactivity-optimization) — **HIGH** - - 3.1 [Avoid Constructors in Components](#31-avoid-constructors-in-components) - - 3.2 [Avoid CSS Classes in Learning Examples](#32-avoid-css-classes-in-learning-examples) - - 3.3 [Avoid Legacy Lifecycle Hooks (did-insert, will-destroy, did-update)](#33-avoid-legacy-lifecycle-hooks-did-insert-will-destroy-did-update) - - 3.4 [Avoid Unnecessary Tracking](#34-avoid-unnecessary-tracking) - - 3.5 [Build Reactive Chains with Dependent Getters](#35-build-reactive-chains-with-dependent-getters) - - 3.6 [Component File Naming and Export Conventions](#36-component-file-naming-and-export-conventions) - - 3.7 [Prefer Named Exports, Fallback to Default for Implicit Template Lookup](#37-prefer-named-exports-fallback-to-default-for-implicit-template-lookup) - - 3.8 [Prevent Memory Leaks in Components](#38-prevent-memory-leaks-in-components) - - 3.9 [Use {{on}} Modifier for Event Handling](#39-use-on-modifier-for-event-handling) - - 3.10 [Use @cached for Expensive Getters](#310-use-cached-for-expensive-getters) - - 3.11 [Use Class Fields for Component Composition](#311-use-class-fields-for-component-composition) - - 3.12 [Use Component Composition Patterns](#312-use-component-composition-patterns) - - 3.13 [Use Glimmer Components Over Classic Components](#313-use-glimmer-components-over-classic-components) - - 3.14 [Use Native Forms with Platform Validation](#314-use-native-forms-with-platform-validation) - - 3.15 [Use Strict Mode and Template-Only Components](#315-use-strict-mode-and-template-only-components) - - 3.16 [Use Tracked Toolbox for Complex State](#316-use-tracked-toolbox-for-complex-state) - - 3.17 [Validate Component Arguments](#317-validate-component-arguments) -4. [Accessibility Best Practices](#4-accessibility-best-practices) — **HIGH** - - 4.1 [Announce Route Transitions to Screen Readers](#41-announce-route-transitions-to-screen-readers) - - 4.2 [Form Labels and Error Announcements](#42-form-labels-and-error-announcements) - - 4.3 [Keyboard Navigation Support](#43-keyboard-navigation-support) - - 4.4 [Semantic HTML and ARIA Attributes](#44-semantic-html-and-aria-attributes) - - 4.5 [Use ember-a11y-testing for Automated Checks](#45-use-ember-a11y-testing-for-automated-checks) -5. [Service and State Management](#5-service-and-state-management) — **MEDIUM-HIGH** - - 5.1 [Cache API Responses in Services](#51-cache-api-responses-in-services) - - 5.2 [Implement Robust Data Requesting Patterns](#52-implement-robust-data-requesting-patterns) - - 5.3 [Manage Service Owner and Linkage Patterns](#53-manage-service-owner-and-linkage-patterns) - - 5.4 [Optimize WarpDrive Queries](#54-optimize-warpdrive-queries) - - 5.5 [Use Services for Shared State](#55-use-services-for-shared-state) -6. [Template Optimization](#6-template-optimization) — **MEDIUM** - - 6.1 [Avoid Heavy Computation in Templates](#61-avoid-heavy-computation-in-templates) - - 6.2 [Compose Helpers for Reusable Logic](#62-compose-helpers-for-reusable-logic) - - 6.3 [Import Helpers Directly in Templates](#63-import-helpers-directly-in-templates) - - 6.4 [No helper() Wrapper for Plain Functions](#64-no-helper-wrapper-for-plain-functions) - - 6.5 [Optimize Conditional Rendering](#65-optimize-conditional-rendering) - - 6.6 [Template-Only Components with In-Scope Functions](#66-template-only-components-with-in-scope-functions) - - 6.7 [Use {{#each}} with @key for Lists](#67-use-each-with-key-for-lists) - - 6.8 [Use {{#let}} to Avoid Recomputation](#68-use-let-to-avoid-recomputation) - - 6.9 [Use {{fn}} for Partial Application Only](#69-use-fn-for-partial-application-only) - - 6.10 [Use Helper Libraries Effectively](#610-use-helper-libraries-effectively) -7. [Performance Optimization](#7-performance-optimization) — **MEDIUM** - - 7.1 [Use {{on}} Modifier Instead of Event Handler Properties](#71-use-on-modifier-instead-of-event-handler-properties) -8. [Testing Best Practices](#8-testing-best-practices) — **MEDIUM** - - 8.1 [MSW (Mock Service Worker) Setup for Testing](#81-msw-mock-service-worker-setup-for-testing) - - 8.2 [Provide DOM-Abstracted Test Utilities for Library Components](#82-provide-dom-abstracted-test-utilities-for-library-components) - - 8.3 [Use Appropriate Render Patterns in Tests](#83-use-appropriate-render-patterns-in-tests) - - 8.4 [Use Modern Testing Patterns](#84-use-modern-testing-patterns) - - 8.5 [Use qunit-dom for Better Test Assertions](#85-use-qunit-dom-for-better-test-assertions) - - 8.6 [Use Test Waiters for Async Operations](#86-use-test-waiters-for-async-operations) -9. [Tooling and Configuration](#9-tooling-and-configuration) — **MEDIUM** - - 9.1 [VSCode Extensions and MCP Configuration for Ember Projects](#91-vscode-extensions-and-mcp-configuration-for-ember-projects) -10. [Advanced Patterns](#10-advanced-patterns) — **MEDIUM-HIGH** - - 10.1 [Use Ember Concurrency Correctly - User Concurrency Not Data Loading](#101-use-ember-concurrency-correctly---user-concurrency-not-data-loading) - - 10.2 [Use Ember Concurrency for User Input Concurrency](#102-use-ember-concurrency-for-user-input-concurrency) - - 10.3 [Use Helper Functions for Reusable Logic](#103-use-helper-functions-for-reusable-logic) - - 10.4 [Use Modifiers for DOM Side Effects](#104-use-modifiers-for-dom-side-effects) - - 10.5 [Use Reactive Collections from @ember/reactive/collections](#105-use-reactive-collections-from-emberreactivecollections) - ---- - -## 1. Route Loading and Data Fetching - -**Impact: CRITICAL** - -Efficient route loading and parallel data fetching eliminate waterfalls. Using route model hooks effectively and loading data in parallel yields the largest performance gains. - -### 1.1 Implement Smart Route Model Caching - -**Impact: MEDIUM-HIGH (Reduce redundant API calls and improve UX)** - -Implement intelligent model caching strategies to reduce redundant API calls and improve user experience. - -**Incorrect: always fetches fresh data** - -```glimmer-js -// app/routes/post.gjs -import Route from '@ember/routing/route'; -import { service } from '@ember/service'; - -export default class PostRoute extends Route { - @service store; - - model(params) { - // Always makes API call, even if we just loaded this post - return this.store.request({ url: `/posts/${params.post_id}` }); - } - - -} -``` - -**Correct: with smart caching** - -```glimmer-js -// app/routes/post.gjs -import Route from '@ember/routing/route'; -import { service } from '@ember/service'; - -export default class PostRoute extends Route { - @service store; - - model(params) { - // Check cache first - const cached = this.store.cache.peek({ - type: 'post', - id: params.post_id - }); - - // Return cached if fresh (less than 5 minutes old) - if (cached && this.isCacheFresh(cached)) { - return cached; - } - - // Fetch fresh data - return this.store.request({ - url: `/posts/${params.post_id}`, - options: { reload: true } - }); - } - - isCacheFresh(record) { - const cacheTime = record.meta?.cachedAt || 0; - const fiveMinutes = 5 * 60 * 1000; - return (Date.now() - cacheTime) < fiveMinutes; - } - - -} -``` - -**Service-based caching layer:** - -```glimmer-js -// app/routes/post.gjs -import Route from '@ember/routing/route'; -import { service } from '@ember/service'; - -export default class PostRoute extends Route { - @service postCache; - - model(params) { - return this.postCache.getPost(params.post_id); - } - - // Refresh data when returning to route - async activate() { - super.activate(...arguments); - const params = this.paramsFor('post'); - await this.postCache.getPost(params.post_id, { forceRefresh: true }); - } - - -} -``` - -**Using query params for cache control:** - -```glimmer-js -// app/routes/posts.gjs -import Route from '@ember/routing/route'; -import { service } from '@ember/service'; - -export default class PostsRoute extends Route { - @service store; - - queryParams = { - refresh: { refreshModel: true } - }; - - model(params) { - const options = params.refresh - ? { reload: true } - : { backgroundReload: true }; - - return this.store.request({ - url: '/posts', - options - }); - } - - -} -``` - -**Background refresh pattern:** - -```glimmer-js -// app/routes/dashboard.gjs -import Route from '@ember/routing/route'; -import { service } from '@ember/service'; - -export default class DashboardRoute extends Route { - @service store; - - async model() { - // Return cached data immediately - const cached = this.store.cache.peek({ type: 'dashboard' }); - - // Refresh in background - this.store.request({ - url: '/dashboard', - options: { backgroundReload: true } - }); - - return cached || this.store.request({ url: '/dashboard' }); - } - - -} -``` - -Smart caching reduces server load, improves perceived performance, and provides better offline support while keeping data fresh. - -Reference: [https://warp-drive.io/](https://warp-drive.io/) - -### 1.2 Parallel Data Loading in Model Hooks - -**Impact: CRITICAL (2-10× improvement)** - -When fetching multiple independent data sources in a route's model hook, use `Promise.all()` or RSVP.hash() to load them in parallel instead of sequentially. - -`export default` in these route examples is intentional because route modules are discovered through resolver lookup. In hybrid `.gjs`/`.hbs` codebases, keep route defaults and add named exports only when you need explicit imports elsewhere. - -**Incorrect: sequential loading, 3 round trips** - -```javascript -// app/routes/dashboard.js -import Route from '@ember/routing/route'; -import { service } from '@ember/service'; - -export default class DashboardRoute extends Route { - @service store; - - async model() { - const user = await this.store.request({ url: '/users/me' }); - const posts = await this.store.request({ url: '/posts?recent=true' }); - const notifications = await this.store.request({ url: '/notifications?unread=true' }); - - return { user, posts, notifications }; - } -} -``` - -**Correct: parallel loading, 1 round trip** - -```javascript -// app/routes/dashboard.js -import Route from '@ember/routing/route'; -import { service } from '@ember/service'; -import { hash } from 'rsvp'; - -export default class DashboardRoute extends Route { - @service store; - - model() { - return hash({ - user: this.store.request({ url: '/users/me' }), - posts: this.store.request({ url: '/posts?recent=true' }), - notifications: this.store.request({ url: '/notifications?unread=true' }), - }); - } -} -``` - -Using `hash()` from RSVP allows Ember to resolve all promises concurrently, significantly reducing load time. - -### 1.3 Use Loading Substates for Better UX - -**Impact: CRITICAL (Perceived performance improvement)** - -Implement loading substates to show immediate feedback while data loads, preventing blank screens and improving perceived performance. - -**Incorrect: no loading state** - -```javascript -// app/routes/posts.js -export default class PostsRoute extends Route { - async model() { - return this.store.request({ url: '/posts' }); - } -} -``` - -**Correct: with loading substate** - -```javascript -// app/routes/posts.js -export default class PostsRoute extends Route { - model() { - // Return promise directly - Ember will show posts-loading template - return this.store.request({ url: '/posts' }); - } -} -``` - -Ember automatically renders `{route-name}-loading` route templates while the model promise resolves, providing better UX without extra code. - -### 1.4 Use Route Templates with Co-located Syntax - -**Impact: MEDIUM-HIGH (Better code organization and maintainability)** - -Use co-located route templates with modern gjs syntax for better organization and maintainability. - -**Incorrect: separate template file - old pattern** - -```glimmer-js -// app/routes/posts.js (separate file) -import Route from '@ember/routing/route'; - -export default class PostsRoute extends Route { - model() { - return this.store.request({ url: '/posts' }); - } -} - -// app/templates/posts.gjs (separate template file) - -``` - -**Correct: co-located route template** - -```glimmer-js -// app/routes/posts.gjs -import Route from '@ember/routing/route'; - -export default class PostsRoute extends Route { - model() { - return this.store.request({ url: '/posts' }); - } - - -} -``` - -**With loading and error states:** - -```glimmer-js -// app/routes/posts.gjs -import Route from '@ember/routing/route'; -import { service } from '@ember/service'; - -export default class PostsRoute extends Route { - @service store; - - model() { - return this.store.request({ url: '/posts' }); - } - - -} -``` - -**Template-only routes:** - -```glimmer-js -// app/routes/about.gjs - -``` - -Co-located route templates keep route logic and presentation together, making the codebase easier to navigate and maintain. - -Reference: [https://guides.emberjs.com/release/routing/](https://guides.emberjs.com/release/routing/) - -### 1.5 Use Route-Based Code Splitting - -**Impact: CRITICAL (30-70% initial bundle reduction)** - -With Embroider's route-based code splitting, routes and their components are automatically split into separate chunks, loaded only when needed. - -**Incorrect: everything in main bundle** - -```javascript -// ember-cli-build.js -const EmberApp = require('ember-cli/lib/broccoli/ember-app'); - -module.exports = function (defaults) { - const app = new EmberApp(defaults, { - // No optimization - }); - - return app.toTree(); -}; -``` - -**Correct: Embroider with Vite and route splitting** - -```javascript -// ember-cli-build.js -const { Vite } = require('@embroider/vite'); - -module.exports = require('@embroider/compat').compatBuild(app, Vite, { - staticAddonTestSupportTrees: true, - staticAddonTrees: true, - staticHelpers: true, - staticModifiers: true, - staticComponents: true, - splitAtRoutes: ['admin', 'reports', 'settings'], // Routes to split -}); -``` - -Embroider with `splitAtRoutes` creates separate bundles for specified routes, reducing initial load time by 30-70%. - -Reference: [https://github.com/embroider-build/embroider](https://github.com/embroider-build/embroider) - ---- - -## 2. Build and Bundle Optimization - -**Impact: CRITICAL** - -Using Embroider with static build optimizations, route-based code splitting, and proper imports reduces bundle size and improves Time to Interactive. - -### 2.1 Avoid Importing Entire Addon Namespaces - -**Impact: CRITICAL (200-500ms import cost reduction)** - -Import specific utilities and components directly rather than entire addon namespaces to enable better tree-shaking and reduce bundle size. - -**Incorrect: imports entire namespace** - -```javascript -import { tracked } from '@glimmer/tracking'; -import Component from '@glimmer/component'; -import { action } from '@ember/object'; -// OK - these are already optimized - -// But avoid this pattern with utility libraries: -import * as lodash from 'lodash'; -import * as moment from 'moment'; - -class My extends Component { - someMethod() { - return lodash.debounce(this.handler, 300); - } -} -``` - -**Correct: direct imports** - -```javascript -import { tracked } from '@glimmer/tracking'; -import Component from '@glimmer/component'; -import { action } from '@ember/object'; -import debounce from 'lodash/debounce'; -import dayjs from 'dayjs'; // moment alternative, smaller - -class My extends Component { - someMethod() { - return debounce(this.handler, 300); - } -} -``` - -**Even better: use Ember utilities when available** - -```javascript -import { tracked } from '@glimmer/tracking'; -import Component from '@glimmer/component'; -import { action } from '@ember/object'; -import { debounce } from '@ember/runloop'; - -class My extends Component { - someMethod() { - return debounce(this, this.handler, 300); - } -} -``` - -Direct imports and using built-in Ember utilities reduce bundle size by avoiding unused code. - -### 2.2 Lazy Load Heavy Dependencies - -**Impact: CRITICAL (30-50% initial bundle reduction)** - -Use dynamic imports to load heavy libraries only when needed, reducing initial bundle size. - -**Incorrect: loaded upfront** - -```javascript -import Component from '@glimmer/component'; -import Chart from 'chart.js/auto'; // 300KB library loaded immediately -import hljs from 'highlight.js'; // 500KB library loaded immediately - -class Dashboard extends Component { - get showChart() { - return this.args.hasData; - } -} -``` - -**Correct: lazy loaded with error/loading state handling** - -```glimmer-js -import Component from '@glimmer/component'; -import { getPromiseState } from 'reactiveweb/promise'; - -class Dashboard extends Component { - // Use getPromiseState to model promise state for error/loading handling - chartLoader = getPromiseState(async () => { - const { default: Chart } = await import('chart.js/auto'); - return Chart; - }); - - highlighterLoader = getPromiseState(async () => { - const { default: hljs } = await import('highlight.js'); - return hljs; - }); - - loadChart = () => { - // Triggers lazy load, handles loading/error states automatically - return this.chartLoader.value; - }; - - highlightCode = (code) => { - const hljs = this.highlighterLoader.value; - if (hljs) { - return hljs.highlightAuto(code); - } - return code; - }; - - -} -``` - -**Note**: Always model promise state (loading/error/resolved) using `getPromiseState` from `reactiveweb/promise` to handle slow networks and errors properly. - -Dynamic imports reduce initial bundle size by 30-50%, improving Time to Interactive. - -### 2.3 Use Embroider Build Pipeline - -**Impact: CRITICAL (Modern build system with better performance)** - -Use Embroider, Ember's modern build pipeline, with Vite for faster builds, better tree-shaking, and smaller bundles. - -**Incorrect: classic build pipeline** - -```javascript -// ember-cli-build.js -const EmberApp = require('ember-cli/lib/broccoli/ember-app'); - -module.exports = function (defaults) { - const app = new EmberApp(defaults, {}); - return app.toTree(); -}; -``` - -**Correct: Embroider with Vite** - -```javascript -// ember-cli-build.js -const EmberApp = require('ember-cli/lib/broccoli/ember-app'); -const { compatBuild } = require('@embroider/compat'); - -module.exports = async function (defaults) { - const { buildOnce } = await import('@embroider/vite'); - - let app = new EmberApp(defaults, { - // Add options here - }); - - return compatBuild(app, buildOnce); -}; -``` - -**For stricter static analysis: optimized mode** - -```javascript -// ember-cli-build.js -const EmberApp = require('ember-cli/lib/broccoli/ember-app'); -const { compatBuild } = require('@embroider/compat'); - -module.exports = async function (defaults) { - const { buildOnce } = await import('@embroider/vite'); - - let app = new EmberApp(defaults, { - // Add options here - }); - - return compatBuild(app, buildOnce, { - // Enable static analysis for better tree-shaking - staticAddonTestSupportTrees: true, - staticAddonTrees: true, - staticHelpers: true, - staticModifiers: true, - staticComponents: true, - }); -}; -``` - -Embroider provides a modern build pipeline with Vite that offers faster builds and better optimization compared to the classic Ember CLI build system. - -Reference: [https://github.com/embroider-build/embroider](https://github.com/embroider-build/embroider) - ---- - -## 3. Component and Reactivity Optimization - -**Impact: HIGH** - -Proper use of Glimmer components, modern file conventions, tracked properties, and avoiding unnecessary recomputation improves rendering performance. - -### 3.1 Avoid Constructors in Components - -**Impact: HIGH (Prevents infinite render loops and simplifies code)** - -**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. - -- [Ember Octane Guide](https://guides.emberjs.com/release/upgrading/current-edition/) - -- [warp-drive/reactiveweb](https://github.com/emberjs/data/tree/main/packages/reactiveweb) - -**Incorrect: using constructor** - -```glimmer-js -// 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; - } - } - - -} -``` - -**Correct: use class fields and declarative async state** - -```glimmer-js -// 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}`, - }); - } - - -} -``` - -**When You Might Need a Constructor: Very Rare** - -```glimmer-js -// 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 - } - - -} -``` - -Very rarely, you might need a constructor for truly exceptional cases. Even then, use modern patterns: - -**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. - -### 3.2 Avoid CSS Classes in Learning Examples - -**Impact: LOW-MEDIUM (Cleaner, more focused learning materials)** - -Don't add CSS classes to learning content and examples unless they provide actual value above the surrounding context. Classes add visual noise and distract from the concepts being taught. - -**Incorrect: unnecessary classes in learning example** - -```glimmer-js -// app/components/user-card.gjs -import Component from '@glimmer/component'; - -export class UserCard extends Component { - -} -``` - -**Why This Is Wrong:** - -- Classes add visual clutter that obscures the actual concepts - -- Learners focus on naming conventions instead of the pattern being taught - -- Makes copy-paste more work (need to remove or change class names) - -- Implies these specific class names are required or best practice - -- Distracts from structural HTML and component logic - -**Correct: focused on concepts** - -```glimmer-js -// app/components/user-card.gjs -import Component from '@glimmer/component'; - -export class UserCard extends Component { - -} -``` - -**Benefits:** - -- **Clarity**: Easier to understand the component structure - -- **Focus**: Reader attention stays on the concepts being taught - -- **Simplicity**: Less code to process mentally - -- **Flexibility**: Reader can add their own classes without conflict - -- **Reusability**: Examples are easier to adapt to real code - -**When Classes ARE Appropriate in Examples:** - -```glimmer-js -// Example: Teaching about ...attributes for styling flexibility -export class Card extends Component { - -} - -{{! Usage: ... }} -``` - -**When to Include Classes:** - -1. **Teaching class binding** - Example explicitly about conditional classes or class composition - -2. **Demonstrating ...attributes** - Showing how callers add classes - -3. **Accessibility** - Using classes for semantic meaning (e.g., `aria-*` helpers) - -4. **Critical to example** - Class name is essential to understanding (e.g., `selected`, `active`) - -**Examples Where Classes Add Value:** - -```glimmer-js -// Good: Teaching about class composition -import { cn } from 'ember-cn'; - -export class Button extends Component { - -} -``` - -**Default Stance:** - -When writing learning examples or documentation: - -1. **Start without classes** - Add them only if needed - -2. **Ask**: Does this class help explain the concept? - -3. **Remove** any decorative or structural classes that aren't essential - -4. **Use** `...attributes` to show styling flexibility - -**Real-World Context:** - -In production code, you'll have classes for styling. But in learning materials, strip them away unless they're teaching something specific about classes themselves. - -**Common Violations:** - -❌ BEM classes in examples (`user-card__header`) - -❌ Utility classes unless teaching utilities (`flex`, `mt-4`) - -❌ Semantic classes that don't teach anything (`container`, `wrapper`) - -❌ Design system classes unless teaching design system integration - -**Summary:** - -Keep learning examples focused on the concept being taught. CSS classes should appear only when they're essential to understanding the pattern or when demonstrating styling flexibility with `...attributes`. - -Reference: [https://guides.emberjs.com/release/components/](https://guides.emberjs.com/release/components/) - -### 3.3 Avoid Legacy Lifecycle Hooks (did-insert, will-destroy, did-update) - -**Impact: HIGH (Prevents memory leaks and enforces modern patterns)** - -**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. - -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 - -For computed values or reactive transformations, use getters and `@cached`: - -- [Ember Modifiers](https://github.com/ember-modifier/ember-modifier) - -- [warp-drive/reactiveweb](https://github.com/emberjs/data/tree/main/packages/reactiveweb) - -- [Glimmer Tracking](https://guides.emberjs.com/release/in-depth-topics/autotracking-in-depth/) - -**❌ 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}`; - } - - -} -``` - -**✅ 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}`; - } - - -} -``` - -**✅ 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 - }; - } - - -} -``` - -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):** - -```glimmer-js -// app/components/chart.gjs -import chart from '../modifiers/chart'; - - -``` - -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! - } - - -} -``` - -**✅ Correct (Resource with automatic cleanup):** - -```glimmer-js -// 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]); - - -} -``` - -| 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 | - -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}}` - -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 - -❌ **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** - -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.** - -### 3.4 Avoid Unnecessary Tracking - -**Impact: HIGH (20-40% fewer invalidations)** - -Only mark properties as `@tracked` if they need to trigger re-renders when changed. Overusing `@tracked` causes unnecessary invalidations and re-renders. - -**Incorrect: everything tracked** - -```javascript -import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; -import { action } from '@ember/object'; - -class Form extends Component { - @tracked firstName = ''; // Used in template ✓ - @tracked lastName = ''; // Used in template ✓ - @tracked _formId = Date.now(); // Internal, never rendered ✗ - @tracked _validationCache = new Map(); // Internal state ✗ - - @action - validate() { - this._validationCache.set('firstName', this.firstName.length > 0); - // Unnecessary re-render triggered - } -} -``` - -**Correct: selective tracking** - -```javascript -import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; -import { action } from '@ember/object'; - -class Form extends Component { - @tracked firstName = ''; // Rendered in template - @tracked lastName = ''; // Rendered in template - @tracked isValid = false; // Rendered status - - _formId = Date.now(); // Not tracked - internal only - _validationCache = new Map(); // Not tracked - internal state - - @action - validate() { - this._validationCache.set('firstName', this.firstName.length > 0); - this.isValid = this._validationCache.get('firstName'); - // Only re-renders when isValid changes - } -} -``` - -Only track properties that directly affect the template or other tracked getters to minimize unnecessary re-renders. - -### 3.5 Build Reactive Chains with Dependent Getters - -**Impact: HIGH (Clear data flow and automatic reactivity)** - -Create reactive chains where getters depend on other getters or tracked properties for clear, maintainable data derivation. - -**Incorrect: imperative updates** - -```glimmer-js -// app/components/shopping-cart.gjs -import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; -import { action } from '@ember/object'; - -class ShoppingCart extends Component { - @tracked items = []; - @tracked subtotal = 0; - @tracked tax = 0; - @tracked shipping = 0; - @tracked total = 0; - - @action - addItem(item) { - this.items = [...this.items, item]; - this.recalculate(); - } - - @action - removeItem(index) { - this.items = this.items.filter((_, i) => i !== index); - this.recalculate(); - } - - recalculate() { - this.subtotal = this.items.reduce((sum, item) => sum + item.price, 0); - this.tax = this.subtotal * 0.08; - this.shipping = this.subtotal > 50 ? 0 : 5.99; - this.total = this.subtotal + this.tax + this.shipping; - } - - -} -``` - -**Correct: reactive getter chains** - -```glimmer-js -// app/components/shopping-cart.gjs -import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; -import { action } from '@ember/object'; -import { TrackedArray } from 'tracked-built-ins'; - -class ShoppingCart extends Component { - @tracked items = new TrackedArray([]); - - // Base calculation - get subtotal() { - return this.items.reduce((sum, item) => sum + item.price, 0); - } - - // Depends on subtotal - get tax() { - return this.subtotal * 0.08; - } - - // Depends on subtotal - get shipping() { - return this.subtotal > 50 ? 0 : 5.99; - } - - // Depends on subtotal, tax, and shipping - get total() { - return this.subtotal + this.tax + this.shipping; - } - - // Derived from total - get formattedTotal() { - return `$${this.total.toFixed(2)}`; - } - - // Multiple dependencies - get discount() { - if (this.items.length >= 5) return this.subtotal * 0.1; - if (this.subtotal > 100) return this.subtotal * 0.05; - return 0; - } - - // Depends on total and discount - get finalTotal() { - return this.total - this.discount; - } - - @action - addItem(item) { - this.items.push(item); - // All getters automatically update! - } - - @action - removeItem(index) { - this.items.splice(index, 1); - // All getters automatically update! - } - - -} -``` - -**Complex reactive chains with @cached:** - -```glimmer-js -// app/components/data-analysis.gjs -import Component from '@glimmer/component'; -import { cached } from '@glimmer/tracking'; - -class DataAnalysis extends Component { - // Base data - get rawData() { - return this.args.data || []; - } - - // Level 1: Filter - @cached - get validData() { - return this.rawData.filter(item => item.value != null); - } - - // Level 2: Transform (depends on validData) - @cached - get normalizedData() { - const max = Math.max(...this.validData.map(d => d.value)); - return this.validData.map(item => ({ - ...item, - normalized: item.value / max - })); - } - - // Level 2: Statistics (depends on validData) - @cached - get statistics() { - const values = this.validData.map(d => d.value); - const sum = values.reduce((a, b) => a + b, 0); - const mean = sum / values.length; - const variance = values.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / values.length; - - return { - count: values.length, - sum, - mean, - stdDev: Math.sqrt(variance), - min: Math.min(...values), - max: Math.max(...values) - }; - } - - // Level 3: Depends on normalizedData and statistics - @cached - get outliers() { - const threshold = this.statistics.mean + (2 * this.statistics.stdDev); - return this.normalizedData.filter(item => item.value > threshold); - } - - // Level 3: Depends on statistics - get qualityScore() { - const validRatio = this.validData.length / this.rawData.length; - const outlierRatio = this.outliers.length / this.validData.length; - return (validRatio * 0.7) + ((1 - outlierRatio) * 0.3); - } - - -} -``` - -**Combining multiple tracked sources:** - -```glimmer-js -// app/components/filtered-list.gjs -import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; -import { cached } from '@glimmer/tracking'; - -class FilteredList extends Component { - @tracked searchTerm = ''; - @tracked selectedCategory = 'all'; - @tracked sortDirection = 'asc'; - - // Depends on args.items and searchTerm - @cached - get searchFiltered() { - if (!this.searchTerm) return this.args.items; - - const term = this.searchTerm.toLowerCase(); - return this.args.items.filter(item => - item.name.toLowerCase().includes(term) || - item.description?.toLowerCase().includes(term) - ); - } - - // Depends on searchFiltered and selectedCategory - @cached - get categoryFiltered() { - if (this.selectedCategory === 'all') return this.searchFiltered; - - return this.searchFiltered.filter(item => - item.category === this.selectedCategory - ); - } - - // Depends on categoryFiltered and sortDirection - @cached - get sorted() { - const items = [...this.categoryFiltered]; - const direction = this.sortDirection === 'asc' ? 1 : -1; - - return items.sort((a, b) => - direction * a.name.localeCompare(b.name) - ); - } - - // Final result - get items() { - return this.sorted; - } - - // Metadata derived from chain - get resultsCount() { - return this.items.length; - } - - get hasFilters() { - return this.searchTerm || this.selectedCategory !== 'all'; - } - - -} -``` - -Reactive getter chains provide automatic updates, clear data dependencies, and better performance through intelligent caching with @cached. - -Reference: [https://guides.emberjs.com/release/in-depth-topics/autotracking-in-depth/](https://guides.emberjs.com/release/in-depth-topics/autotracking-in-depth/) - -### 3.6 Component File Naming and Export Conventions - -**Impact: HIGH (Enforces consistent component structure and predictable imports)** - -Follow modern Ember component file conventions: use `.gjs`/`.gts` files with `