Components are the building blocks of modern Ember.js applications. Since Ember Octane, components are now Glimmer components—lighter, faster, and easier to use than ever before. This guide covers everything you need to know about creating powerful, reusable components.
Glimmer Components vs. Classic Components
Ember Octane introduced Glimmer components, which are the modern standard:
// Modern Glimmer Component
import Component from '@glimmer/component';
export default class UserProfile extends Component {
// Component logic here
}Glimmer components offer:
- No internal state management - Use
@trackedinstead - No lifecycle hooks - Use
{{did-insert}}and{{will-destroy}}modifiers - Better performance - No backing classes for template-only components
- Simpler API - Less boilerplate code
Component Structure
Template-Only Components
The simplest components don't need JavaScript:
{{! app/components/avatar.hbs }}
<div class="avatar" style={{this.style}}>
<img src={{@src}} alt={{@alt}} class="avatar-image" />
</div>JavaScript file is optional for simple cases.
JavaScript Components
For logic and computed properties:
// app/components/user-card.js
import Component from '@glimmer/component';
export default class UserCardComponent extends Component {
get displayName() {
const { firstName, lastName } = this.args;
return `${firstName} ${lastName}`;
}
get isPremium() {
return this.args.userTier === 'premium';
}
}{{! app/components/user-card.hbs }}
<div class="user-card {{if this.isPremium 'premium'}}">
<h2>{{this.displayName}}</h2>
<p>{{@email}}</p>
{{yield}}
</div>Arguments (@args)
Arguments are passed to components using the @ prefix:
Passing Arguments
<UserProfile
@name="John Doe"
@email="john@example.com"
@role="admin"
/>Receiving Arguments
// app/components/user-profile.js
import Component from '@glimmer/component';
export default class UserProfileComponent extends Component {
get isAdmin() {
return this.args.role === 'admin';
}
get displayEmail() {
return this.args.email || 'No email provided';
}
}Argument Defaults
Use destructuring for defaults:
export default class ButtonComponent extends Component {
constructor() {
super(...arguments);
this.args = {
variant: 'primary',
size: 'medium',
...this.args
};
}
get classes() {
return `btn btn-${this.args.variant} btn-${this.args.size}`;
}
}Blocks and Yielding
Components can yield content:
{{! app/components/card.hbs }}
<div class="card">
<header class="card-header">
<h3>{{@title}}</h3>
</header>
<div class="card-body">
{{yield}}
</div>
{{#if @hasFooter}}
<footer class="card-footer">
{{yield to="footer"}}
</footer>
{{/if}}
</div>Usage:
<Card @title="Welcome">
<p>This is the main content.</p>
<:footer>
<button>Learn More</button>
</:footer>
</Card>Tracked Properties
Use @tracked for reactive state:
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class CounterComponent extends Component {
@tracked count = 0;
get message() {
return `Count: ${this.count}`;
}
@action
increment() {
this.count++;
}
@action
decrement() {
this.count--;
}
}<div class="counter">
<p>{{this.message}}</p>
<button {{on "click" this.decrement}}>-</button>
<button {{on "click" this.increment}}>+</button>
</div>Tracked Objects and Arrays
For complex state, use tracked objects:
import { tracked } from '@glimmer/tracking';
class Todo {
@tracked title;
@tracked completed = false;
constructor(title) {
this.title = title;
}
toggle() {
this.completed = !this.completed;
}
}
export default class TodoListComponent extends Component {
@tracked todos = [];
@action
addTodo(title) {
this.todos = [...this.todos, new Todo(title)];
}
@action
toggleTodo(todo) {
todo.toggle();
this.todos = [...this.todos]; // Trigger update
}
get completedCount() {
return this.todos.filter(t => t.completed).length;
}
}Modifiers
Modifiers attach behavior to DOM elements:
Built-in Modifiers
{{! Run code when element is inserted }}
<div {{did-insert this.setup}}>...</div>
{{! Run code when element will be destroyed }}
<div {{will-destroy this.cleanup}}>...</div>
{{! Attach event listeners }}
<button {{on "click" this.handleClick}}>Click</button>
{{! autofocus }}
<input {{autofocus}} />
{{! Run code when arguments change }}
<div {{did-update this.onDataChange @data}}>...</div>Custom Modifiers
// app/modifiers/click-outside.js
import { modifier } from 'ember-modifier';
export default modifier(function clickOutside(element, [callback]) {
const handleClick = (event) => {
if (!element.contains(event.target)) {
callback();
}
};
document.addEventListener('click', handleClick);
return () => {
document.removeEventListener('click', handleClick);
};
});Usage:
<div {{click-outside this.close}}>
<p>Click outside to close</p>
</div>Actions
Actions handle user interactions:
import { action } from '@ember/object';
export default class FormComponent extends Component {
@tracked email = '';
@tracked password = '';
@action
updateEmail(event) {
this.email = event.target.value;
}
@action
async handleSubmit(event) {
event.preventDefault();
try {
await this.args.onSubmit({
email: this.email,
password: this.password
});
this.email = '';
this.password = '';
} catch (error) {
console.error('Submission failed:', error);
}
}
@action
handleReset() {
this.email = '';
this.password = '';
}
}<form {{on "submit" this.handleSubmit}}>
<input
type="email"
value={{this.email}}
{{on "input" this.updateEmail}}
/>
<input
type="password"
value={{this.password}}
{{on "input" (fn this.updateField "password")}}
/>
<button type="submit">Submit</button>
<button type="button" {{on "click" this.handleReset}}>Reset</button>
</form>Component Composition
Build complex UIs from simple components:
{{! app/components/data-table.hbs }}
<table class="data-table">
<thead>
<tr>
{{#each @columns as |column|}}
<th>{{column.label}}</th>
{{/each}}
</tr>
</thead>
<tbody>
{{#each @data as |row|}}
<tr>
{{#each @columns as |column|}}
<td>
{{get row column.key}}
</td>
{{/each}}
</tr>
{{/each}}
</tbody>
</table>Usage:
<DataTable
@columns={{array
(hash key="name" label="Name")
(hash key="email" label="Email")
(hash key="role" label="Role")
}}
@data={{this.users}}
/>Contextual Components
Pass components as arguments:
// app/components/list.js
export default class ListComponent extends Component {
get items() {
return this.args.items || [];
}
}{{! app/components/list.hbs }}
<ul class="list">
{{#each this.items as |item|}}
<li>
{{#let (component @itemComponent item=item) as |Item|}}
<Item />
{{/let}}
</li>
{{/each}}
</ul>Usage:
<List
@items={{this.users}}
@itemComponent={{component "user-list-item"}}
/>Error Boundaries
Handle component errors gracefully:
{{#let (component "safe-component") as |Safe|}}
<Safe>
<RiskyComponent @data={{this.data}} />
</Safe>
{{/let}}Performance Optimization
Lazy Loading
// app/routes/index.js
import Route from '@ember/routing/route';
export default class IndexRoute extends Route {
beforeModel() {
// Lazy load heavy components
import('app/components/heavy-component');
}
}Memoization
import { tracked } from '@glimmer/tracking';
import { cached } from '@glimmer/tracking';
export default class ExpensiveComponent extends Component {
@tracked data;
@cached
get processedData() {
// Expensive computation runs only when data changes
return this.data.map(item => {
return transform(item);
});
}
}Testing Components
// tests/integration/components/user-card-test.js
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, click } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | user-card', function(hooks) {
setupRenderingTest(hooks);
test('it renders user information', async function(assert) {
this.set('user', {
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com'
});
await render(hbs`<UserCard @user={{this.user}} />`);
assert.dom('[data-test-name]').hasText('John Doe');
assert.dom('[data-test-email]').hasText('john@example.com');
});
test('it calls onEdit action when clicked', async function(assert) {
assert.expect(1);
this.set('onEdit', (user) => {
assert.deepEqual(user, { name: 'John' });
});
await render(hbs`<UserCard @user={{this.user}} @onEdit={{this.onEdit}} />`);
await click('[data-test-edit-button]');
});
});Best Practices
- 1Keep Components Focused: Each component should do one thing well
- 2Use Template-Only Components: When possible, avoid JavaScript
- 3Prevent Props Mutation: Never mutate
@argsdirectly - 4Use Getters for Computed Values: Leverage
@trackedand getters - 5Document Public APIs: Clearly document arguments and yielded values
- 6Test Thoroughly: Write tests for all user interactions
Conclusion
Ember.js components are powerful and flexible. With Glimmer components and the Octane edition, component development is simpler and more performant than ever. Master these patterns to build maintainable, scalable applications.
Remember: components are the heart of your Ember app. Invest time in making them reusable, well-tested, and performant. Your future self (and your team) will thank you.