Expert Ember.js Development
Write modern, performant Ember.js applications following Ember Octane conventions and current best practices.
Critical First Step: Use context7 MCP
ALWAYS use context7 MCP before writing or editing ANY Ember code - Before generating, modifying, or reviewing any Ember code, you MUST use the context7 MCP to check for relevant documentation. Context7 provides project-specific conventions, architectural patterns, coding standards, and technical decisions that override these general guidelines.
When to Use context7
Use context7 MCP in these situations:
-
Before writing any new Ember component, route, service, or model
-
Before modifying existing Ember code
-
When implementing features that might have project-specific patterns
-
When unsure about architectural decisions
-
Before suggesting refactors or improvements
How to Use context7
// Example: Check for Ember component patterns before creating a component // Use the context7 MCP to search for relevant documentation // Query examples: // - "ember component patterns" // - "ember routing conventions" // - "ember data models" // - "ember testing standards" // - "glimmer component lifecycle"
// Apply the documentation from context7 to your implementation
If context7 returns relevant documentation, follow it EXACTLY even if it conflicts with this skill's general guidance. Project-specific conventions always take precedence.
Core Principles
-
Embrace Ember Octane - Use Glimmer components, tracked properties, and native classes
-
Convention over configuration - Follow Ember's resolver patterns and file structure
-
Use the platform - Prefer native JavaScript features over framework-specific abstractions when possible
-
Composition through services and modifiers - Extract reusable logic into services and UI behavior into modifiers
-
Data down, actions up (DDAU) - Maintain clear data flow patterns
Context7 MCP Workflow
Before implementing any Ember code, follow this workflow:
Query context7 - Search for relevant documentation using specific terms:
-
Component type (e.g., "ember glimmer component")
-
Feature area (e.g., "ember routing", "ember data")
-
Specific pattern (e.g., "form validation", "authentication")
Review results - Read any returned documentation carefully
-
Note project-specific naming conventions
-
Identify required patterns or abstractions
-
Check for mandatory testing requirements
-
Look for deprecated approaches to avoid
Apply documentation - Implement code following context7 guidance
-
Use project-specific utilities and helpers
-
Follow established architectural patterns
-
Match existing code style and structure
-
Include required metadata or annotations
Fall back to general patterns - If context7 has no relevant docs, use this skill's patterns
-
Apply standard Ember Octane conventions
-
Follow community best practices
-
Use examples from this skill as templates
Remember: context7 documentation always overrides this skill's general guidance.
Context7 Query Examples
When implementing different types of Ember code, use these query patterns:
For Components:
- "ember component patterns"
- "ember component architecture"
- "glimmer component conventions"
- "[specific component type]" (e.g., "button component", "form component")
- "component props" or "component arguments"
- "component lifecycle"
- "component testing"
For Routes:
- "ember routing patterns"
- "ember route conventions"
- "route data loading"
- "route guards" or "route authentication"
- "nested routes"
- "query parameters"
For Ember Data:
- "ember data models"
- "ember data adapters"
- "ember data serializers"
- "api integration"
- "data relationships"
- "data layer patterns"
For Services:
- "ember services"
- "service patterns"
- "shared state management"
- "[specific service]" (e.g., "authentication service", "api service")
For Testing:
- "ember testing"
- "component testing"
- "integration tests"
- "acceptance tests"
- "test patterns"
- "test data" or "fixtures"
For Styling:
- "ember styling"
- "css conventions"
- "component styles"
- "tailwind" or "[css framework]"
When Editing Existing Code:
- Search for the specific feature: "user profile", "login form", etc.
- Search for the pattern you're implementing: "form validation", "dropdown menu"
- Search for utilities you might need: "validation utilities", "date helpers"
Modern Ember Patterns (Octane+)
Glimmer Components
// app/components/user-profile.js import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import { inject as service } from '@ember/service';
export default class UserProfileComponent extends Component { @service currentUser; @service router;
@tracked isEditing = false; @tracked formData = null;
constructor(owner, args) { super(owner, args); // Use constructor for one-time setup this.formData = { ...this.args.user }; }
@action toggleEdit() { this.isEditing = !this.isEditing; if (this.isEditing) { this.formData = { ...this.args.user }; } }
@action async saveUser(event) { event.preventDefault();
try {
await this.args.onSave(this.formData);
this.isEditing = false;
} catch (error) {
// Handle error
console.error('Save failed:', error);
}
}
@action updateField(field, event) { this.formData = { ...this.formData, [field]: event.target.value }; } }
{{! app/components/user-profile.hbs }} <div class="user-profile"> {{#if this.isEditing}} <form {{on "submit" this.saveUser}}> <label> Name: <input type="text" value={{this.formData.name}} {{on "input" (fn this.updateField "name")}} /> </label>
<label>
Email:
<input
type="email"
value={{this.formData.email}}
{{on "input" (fn this.updateField "email")}}
/>
</label>
<button type="submit">Save</button>
<button type="button" {{on "click" this.toggleEdit}}>Cancel</button>
</form>
{{else}} <div class="profile-display"> <h2>{{@user.name}}</h2> <p>{{@user.email}}</p>
{{#if this.currentUser.canEdit}}
<button {{on "click" this.toggleEdit}}>Edit Profile</button>
{{/if}}
</div>
{{/if}} </div>
Tracked Properties and Getters
import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { cached } from '@glimmer/tracking';
export default class DataGridComponent extends Component { @tracked sortColumn = 'name'; @tracked sortDirection = 'asc'; @tracked filterText = '';
// Use @cached for expensive computations that depend on tracked properties @cached get filteredData() { const { filterText } = this; if (!filterText) return this.args.data;
const lower = filterText.toLowerCase();
return this.args.data.filter(item =>
item.name.toLowerCase().includes(lower) ||
item.email.toLowerCase().includes(lower)
);
}
@cached get sortedData() { const data = [...this.filteredData]; const { sortColumn, sortDirection } = this;
return data.sort((a, b) => {
const aVal = a[sortColumn];
const bVal = b[sortColumn];
const result = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
return sortDirection === 'asc' ? result : -result;
});
} }
Services
// app/services/notification.js import Service from '@ember/service'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object';
export default class NotificationService extends Service { @tracked notifications = [];
@action add(message, type = 'info', duration = 5000) { const id = Date.now() + Math.random(); const notification = { id, message, type };
this.notifications = [...this.notifications, notification];
if (duration > 0) {
setTimeout(() => this.remove(id), duration);
}
return id;
}
@action remove(id) { this.notifications = this.notifications.filter(n => n.id !== id); }
@action success(message, duration) { return this.add(message, 'success', duration); }
@action error(message, duration = 10000) { return this.add(message, 'error', duration); }
@action clear() { this.notifications = []; } }
Custom Modifiers
// app/modifiers/click-outside.js import { modifier } from 'ember-modifier';
export default modifier((element, [callback]) => { function handleClick(event) { if (!element.contains(event.target)) { callback(event); } }
document.addEventListener('click', handleClick, true);
return () => { document.removeEventListener('click', handleClick, true); }; });
{{! Usage }} <div {{click-outside this.closeDropdown}} class="dropdown"> {{! dropdown content }} </div>
Routing
Route Definitions
// app/router.js import EmberRouter from '@ember/routing/router'; import config from 'my-app/config/environment';
export default class Router extends EmberRouter { location = config.locationType; rootURL = config.rootURL; }
Router.map(function () { this.route('dashboard', { path: '/' });
this.route('users', function () { this.route('index', { path: '/' }); this.route('new'); this.route('user', { path: '/:user_id' }, function () { this.route('edit'); this.route('settings'); }); });
this.route('not-found', { path: '/*path' }); });
Route Class
// app/routes/users/user.js import Route from '@ember/routing/route'; import { inject as service } from '@ember/service';
export default class UsersUserRoute extends Route { @service store; @service router;
async model(params) { try { return await this.store.findRecord('user', params.user_id, { include: 'profile,settings' }); } catch (error) { if (error.errors?.[0]?.status === '404') { this.router.transitionTo('not-found'); } throw error; } }
// Redirect if user doesn't have permission afterModel(model) { if (!this.currentUser.canViewUser(model)) { this.router.transitionTo('dashboard'); } }
// Reset controller state on exit resetController(controller, isExiting) { if (isExiting) { controller.setProperties({ queryParams: {}, isEditing: false }); } } }
Loading and Error States
// app/routes/users/user/loading.js import Route from '@ember/routing/route';
export default class UsersUserLoadingRoute extends Route {}
{{! app/templates/users/user/loading.hbs }} <div class="loading-spinner"> <p>Loading user...</p> </div>
Ember Data
Models
// app/models/user.js import Model, { attr, hasMany, belongsTo } from '@ember-data/model';
export default class UserModel extends Model { @attr('string') name; @attr('string') email; @attr('date') createdAt; @attr('boolean', { defaultValue: true }) isActive; @attr('number') loginCount;
@belongsTo('profile', { async: true, inverse: 'user' }) profile; @hasMany('post', { async: true, inverse: 'author' }) posts;
// Computed properties still work but use native getters get displayName() { return this.name || this.email?.split('@')[0] || 'Anonymous'; }
get isNewUser() { const daysSinceCreation = (Date.now() - this.createdAt) / (1000 * 60 * 60 * 24); return daysSinceCreation < 7; } }
Custom Adapters
// app/adapters/application.js import JSONAPIAdapter from '@ember-data/adapter/json-api'; import { inject as service } from '@ember/service';
export default class ApplicationAdapter extends JSONAPIAdapter { @service session;
host = 'https://api.example.com'; namespace = 'v1';
get headers() { const headers = {};
if (this.session.isAuthenticated) {
headers['Authorization'] = `Bearer ${this.session.data.authenticated.token}`;
}
return headers;
}
handleResponse(status, headers, payload, requestData) { if (status === 401) { this.session.invalidate(); }
return super.handleResponse(status, headers, payload, requestData);
} }
Custom Serializers
// app/serializers/application.js import JSONAPISerializer from '@ember-data/serializer/json-api';
export default class ApplicationSerializer extends JSONAPISerializer { // Normalize date strings to Date objects normalizeDateFields(hash) { const dateFields = ['createdAt', 'updatedAt', 'publishedAt'];
dateFields.forEach(field => {
if (hash[field]) {
hash[field] = new Date(hash[field]);
}
});
return hash;
}
normalize(modelClass, resourceHash) { this.normalizeDateFields(resourceHash.attributes || {}); return super.normalize(modelClass, resourceHash); } }
Testing
Component Integration Tests
// tests/integration/components/user-profile-test.js import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render, click, fillIn } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | user-profile', function (hooks) { setupRenderingTest(hooks);
test('it displays user information', async function (assert) { this.set('user', { name: 'Jane Doe', email: 'jane@example.com' });
await render(hbs`<UserProfile @user={{this.user}} />`);
assert.dom('h2').hasText('Jane Doe');
assert.dom('p').hasText('jane@example.com');
});
test('it allows editing when canEdit is true', async function (assert) { this.owner.lookup('service:current-user').canEdit = true; this.set('user', { name: 'Jane Doe', email: 'jane@example.com' }); this.set('onSave', () => {});
await render(hbs`
<UserProfile @user={{this.user}} @onSave={{this.onSave}} />
`);
await click('button:contains("Edit Profile")');
assert.dom('form').exists();
assert.dom('input[type="text"]').hasValue('Jane Doe');
await fillIn('input[type="text"]', 'Jane Smith');
await click('button[type="submit"]');
assert.dom('form').doesNotExist();
}); });
Route/Acceptance Tests
// tests/acceptance/user-flow-test.js import { module, test } from 'qunit'; import { visit, currentURL, click, fillIn } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support';
module('Acceptance | user flow', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks);
test('visiting /users and creating a new user', async function (assert) { await visit('/users');
assert.strictEqual(currentURL(), '/users');
assert.dom('h1').hasText('Users');
await click('a:contains("New User")');
assert.strictEqual(currentURL(), '/users/new');
await fillIn('[data-test-name-input]', 'John Doe');
await fillIn('[data-test-email-input]', 'john@example.com');
await click('[data-test-submit]');
assert.strictEqual(currentURL(), '/users/1');
assert.dom('[data-test-user-name]').hasText('John Doe');
}); });
Performance Optimization
See references/performance.md for comprehensive optimization strategies.
Quick Reference
Problem Solution
Unnecessary component re-renders Use @cached for expensive getters
Large lists Use ember-collection or virtual scrolling
Slow Ember Data queries Optimize includes, use custom serializers
Bundle size Use route-based code splitting, lazy engines
Memory leaks Properly clean up in willDestroy, cancel timers
Critical Anti-patterns
// ❌ Mutating tracked properties directly this.items.push(newItem); // Won't trigger reactivity
// ✅ Replace the entire array this.items = [...this.items, newItem];
// ❌ Creating new functions in templates {{on "click" (fn this.handleClick item)}}
// ✅ Use actions or stable references @action handleItemClick(item) { /* ... */ } {{on "click" (fn this.handleItemClick item)}}
// ❌ Not using @cached for expensive computations get expensiveComputation() { return this.data.filter(/* complex logic */); }
// ✅ Use @cached @cached get expensiveComputation() { return this.data.filter(/* complex logic */); }
Project Structure
app/ ├── components/ # Glimmer components │ └── user-profile/ │ ├── component.js │ ├── index.hbs │ └── styles.css ├── controllers/ # Controllers (use sparingly in Octane) ├── helpers/ # Template helpers ├── modifiers/ # Custom modifiers ├── models/ # Ember Data models ├── routes/ # Route classes ├── services/ # Services ├── templates/ # Route templates ├── adapters/ # Ember Data adapters ├── serializers/ # Ember Data serializers ├── styles/ # Global styles └── app.js
tests/ ├── integration/ # Component tests ├── unit/ # Unit tests (models, services, etc.) └── acceptance/ # Full application tests
Tooling Recommendations
Category Tool Notes
CLI ember-cli Official tooling
Testing QUnit + ember-qunit Built-in, well integrated
Linting ESLint + ember-template-lint Catch template issues
Formatting Prettier Use with ember-template-lint
Mocking ember-cli-mirage API mocking for tests
State management Services + tracked Built-in, no extra deps
HTTP fetch or ember-fetch Native or polyfilled
Common Patterns
Form Handling with Validation
// app/components/registration-form.js import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import { inject as service } from '@ember/service';
export default class RegistrationFormComponent extends Component { @service notification;
@tracked email = ''; @tracked password = ''; @tracked confirmPassword = ''; @tracked errors = {}; @tracked isSubmitting = false;
get isValid() { return ( this.email && this.password.length >= 8 && this.password === this.confirmPassword && Object.keys(this.errors).length === 0 ); }
@action updateEmail(event) { this.email = event.target.value; this.validateEmail(); }
@action updatePassword(event) { this.password = event.target.value; this.validatePassword(); }
@action updateConfirmPassword(event) { this.confirmPassword = event.target.value; this.validateConfirmPassword(); }
validateEmail() { const emailRegex = /^[^\s@]+@[^\s@]+.[^\s@]+$/;
if (!this.email) {
this.errors = { ...this.errors, email: 'Email is required' };
} else if (!emailRegex.test(this.email)) {
this.errors = { ...this.errors, email: 'Invalid email format' };
} else {
const { email, ...rest } = this.errors;
this.errors = rest;
}
}
validatePassword() { if (this.password.length < 8) { this.errors = { ...this.errors, password: 'Password must be at least 8 characters' }; } else { const { password, ...rest } = this.errors; this.errors = rest; }
// Re-validate confirm password if it's filled
if (this.confirmPassword) {
this.validateConfirmPassword();
}
}
validateConfirmPassword() { if (this.password !== this.confirmPassword) { this.errors = { ...this.errors, confirmPassword: 'Passwords do not match' }; } else { const { confirmPassword, ...rest } = this.errors; this.errors = rest; } }
@action async submit(event) { event.preventDefault();
if (!this.isValid) return;
this.isSubmitting = true;
try {
await this.args.onSubmit({
email: this.email,
password: this.password
});
this.notification.success('Registration successful!');
} catch (error) {
this.notification.error(error.message || 'Registration failed');
} finally {
this.isSubmitting = false;
}
} }
Infinite Scroll with Modifier
// app/modifiers/infinite-scroll.js import { modifier } from 'ember-modifier';
export default modifier((element, [callback], { threshold = 200 }) => { let isLoading = false;
function handleScroll() { if (isLoading) return;
const { scrollTop, scrollHeight, clientHeight } = element;
const distanceFromBottom = scrollHeight - (scrollTop + clientHeight);
if (distanceFromBottom < threshold) {
isLoading = true;
callback().finally(() => {
isLoading = false;
});
}
}
element.addEventListener('scroll', handleScroll, { passive: true });
return () => { element.removeEventListener('scroll', handleScroll); }; });
TypeScript Support
Ember has strong TypeScript support. Enable it with:
ember install ember-cli-typescript
// app/components/user-profile.ts import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import type { TOC } from '@ember/component/template-only';
interface UserProfileArgs { user: { name: string; email: string; avatarUrl?: string; }; onSave: (data: UserData) => Promise<void>; canEdit?: boolean; }
interface UserData { name: string; email: string; }
export default class UserProfileComponent extends Component<UserProfileArgs> { @tracked isEditing = false; @tracked formData: UserData | null = null;
@action async saveUser(event: SubmitEvent): Promise<void> { event.preventDefault();
if (!this.formData) return;
await this.args.onSave(this.formData);
this.isEditing = false;
} }
// Template-only component signature export interface GreetingSignature { Element: HTMLDivElement; Args: { name: string; }; }
const Greeting: TOC<GreetingSignature> = <template> <div ...attributes>Hello {{@name}}!</div> </template>;
export default Greeting;
Remember: Always Use context7 MCP First!
Before implementing any Ember code, query the context7 MCP for relevant project documentation. Context7 provides project-specific guidelines that always supersede these general best practices.
Quick context7 Query Guide
Before writing components:
- Query: "ember component patterns", "glimmer component", "component architecture"
Before routing work:
- Query: "ember routing", "route patterns", "navigation"
Before Ember Data:
- Query: "ember data models", "api integration", "data layer"
Before tests:
- Query: "ember testing", "test patterns", "test requirements"
When editing existing code:
- Query the specific feature or pattern you're working with
The context7 MCP is your source of truth for this project's Ember conventions.