7.3 KiB
title, impact, impactDescription, tags
| title | impact | impactDescription | tags |
|---|---|---|---|
| Use Reactive Collections from @ember/reactive/collections | HIGH | Enables reactive arrays, maps, and sets | reactivity, tracked, collections, advanced |
Use Reactive Collections from @ember/reactive/collections
Use reactive collections from @ember/reactive/collections to make arrays, Maps, and Sets reactive in Ember. Standard JavaScript collections don't trigger Ember's reactivity system when mutated—reactive collections solve this.
The Problem: Standard arrays, Maps, and Sets are not reactive in Ember when you mutate them. Changes won't trigger template updates.
The Solution:
Use Ember's built-in reactive collections from @ember/reactive/collections.
Reactive Arrays
Incorrect (non-reactive array):
// app/components/todo-list.gjs
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class TodoList extends Component {
@tracked todos = []; // ❌ Array mutations (push, splice, etc.) won't trigger updates
@action
addTodo(text) {
// This won't trigger a re-render!
this.todos.push({ id: Date.now(), text });
}
@action
removeTodo(id) {
// This also won't trigger a re-render!
const index = this.todos.findIndex((t) => t.id === id);
this.todos.splice(index, 1);
}
<template>
<ul>
{{#each this.todos as |todo|}}
<li>
{{todo.text}}
<button {{on "click" (fn this.removeTodo todo.id)}}>Remove</button>
</li>
{{/each}}
</ul>
<button {{on "click" (fn this.addTodo "New todo")}}>Add</button>
</template>
}
Correct (reactive array with @ember/reactive/collections):
// app/components/todo-list.gjs
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { trackedArray } from '@ember/reactive/collections';
export default class TodoList extends Component {
todos = trackedArray([]); // ✅ Mutations are reactive
@action
addTodo(text) {
// Now this triggers re-render!
this.todos.push({ id: Date.now(), text });
}
@action
removeTodo(id) {
// This also triggers re-render!
const index = this.todos.findIndex((t) => t.id === id);
this.todos.splice(index, 1);
}
<template>
<ul>
{{#each this.todos as |todo|}}
<li>
{{todo.text}}
<button {{on "click" (fn this.removeTodo todo.id)}}>Remove</button>
</li>
{{/each}}
</ul>
<button {{on "click" (fn this.addTodo "New todo")}}>Add</button>
</template>
}
Reactive Maps
Maps are useful for key-value stores with non-string keys:
// app/components/user-cache.gjs
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { trackedMap } from '@ember/reactive/collections';
export default class UserCache extends Component {
userCache = trackedMap(); // key: userId, value: userData
@action
cacheUser(userId, userData) {
this.userCache.set(userId, userData);
}
@action
clearUser(userId) {
this.userCache.delete(userId);
}
get cachedUsers() {
return Array.from(this.userCache.values());
}
<template>
<ul>
{{#each this.cachedUsers as |user|}}
<li>{{user.name}}</li>
{{/each}}
</ul>
<p>Cache size: {{this.userCache.size}}</p>
</template>
}
Reactive Sets
Sets are useful for unique collections:
// app/components/tag-selector.gjs
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { trackedSet } from '@ember/reactive/collections';
export default class TagSelector extends Component {
selectedTags = trackedSet();
@action
toggleTag(tag) {
if (this.selectedTags.has(tag)) {
this.selectedTags.delete(tag);
} else {
this.selectedTags.add(tag);
}
}
get selectedCount() {
return this.selectedTags.size;
}
<template>
<div>
{{#each @availableTags as |tag|}}
<label>
<input
type="checkbox"
checked={{this.selectedTags.has tag}}
{{on "change" (fn this.toggleTag tag)}}
/>
{{tag}}
</label>
{{/each}}
</div>
<p>Selected: {{this.selectedCount}} tags</p>
</template>
}
When to Use Each Type
| Type | Use Case |
|---|---|
trackedArray |
Ordered lists that need mutation methods (push, pop, splice, etc.) |
trackedMap |
Key-value pairs with non-string keys or when you need size |
trackedSet |
Unique values, membership testing |
Common Patterns
Initialize with data:
import { trackedArray, trackedMap, trackedSet } from '@ember/reactive/collections';
// Array
const todos = trackedArray([
{ id: 1, text: 'First' },
{ id: 2, text: 'Second' },
]);
// Map
const userMap = trackedMap([
[1, { name: 'Alice' }],
[2, { name: 'Bob' }],
]);
// Set
const tags = trackedSet(['javascript', 'ember', 'web']);
Convert to plain JavaScript:
// Array
const plainArray = [...trackedArray];
const plainArray2 = Array.from(trackedArray);
// Map
const plainObject = Object.fromEntries(trackedMap);
// Set
const plainArray3 = [...trackedSet];
Functional array methods still work:
const todos = trackedArray([...]);
// All of these work and are reactive
const completed = todos.filter(t => t.done);
const titles = todos.map(t => t.title);
const allDone = todos.every(t => t.done);
const firstIncomplete = todos.find(t => !t.done);
Alternative: Immutable Updates
If you prefer immutability, you can use regular @tracked with reassignment:
import { tracked } from '@glimmer/tracking';
export default class TodoList extends Component {
@tracked todos = [];
@action
addTodo(text) {
// Reassignment is reactive
this.todos = [...this.todos, { id: Date.now(), text }];
}
@action
removeTodo(id) {
// Reassignment is reactive
this.todos = this.todos.filter((t) => t.id !== id);
}
}
When to use each approach:
- Use reactive collections when you need mutable operations (better performance for large lists)
- Use immutable updates when you want simpler mental model or need history/undo
Best Practices
- Don't mix approaches - choose either reactive collections or immutable updates
- Initialize in class field - no need for constructor
- Use appropriate type - Map for key-value, Set for unique values, Array for ordered lists
- Export from modules if shared across components
Reactive collections from @ember/reactive/collections provide the best of both worlds: mutable operations with full reactivity. They're especially valuable for large lists or frequent updates where immutable updates would be expensive.
References: