# 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 `