What is @ember/reactive in Ember.js? A Complete Guide to Tracked Collections
My Ember template wasn’t updating when I pushed items to an array. I spent hours debugging, checking my @tracked decorators, and wondering if I’d broken Glimmer’s autotracking somehow.
import { tracked } from '@glimmer/tracking';
export default class UserList extends Component { @tracked users = [];
addUser(user) { this.users.push(user); // Template doesn't update! }}The issue? Native JavaScript arrays don’t integrate with Ember’s tracking system. When I mutated the array with push(), Glimmer had no idea anything changed.
The Manual Tracking Workaround
Before I found the proper solution, I tried a hacky workaround:
export default class UserList extends Component { @tracked users = [];
addUser(user) { this.users = [...this.users, user]; // Creates new array, triggers tracking }}This worked, but it felt wrong. Every mutation required creating a new array copy. My Map and Set usages had similar problems, requiring even more verbose workarounds.
I also tried the @action decorator approach:
import { action } from '@ember/object';
export default class UserList extends Component { @tracked users = [];
@action addUser(user) { this.users.push(user); // Still doesn't work - @action doesn't help with collection mutations }}That didn’t work either. @action is for event handlers, not for triggering reactivity.
The Real Solution: @ember/reactive
Then I discovered @ember/reactive, introduced in Ember 6.8 via RFC 1068. It provides tracked collection wrappers that automatically notify Glimmer when mutations occur.
import { TrackedArray } from '@ember/reactive';
export default class UserList extends Component { users = new TrackedArray(); // No @tracked needed!
addUser(user) { this.users.push(user); // Template updates automatically }}No decorators. No manual tracking. No spread operators for simple additions.
Available Tracked Collections
The package exports five collection types:
| Collection | Description |
|---|---|
TrackedArray | Drop-in replacement for native Array |
TrackedMap | Drop-in replacement for native Map |
TrackedSet | Drop-in replacement for native Set |
TrackedWeakMap | Drop-in replacement for native WeakMap |
TrackedWeakSet | Drop-in replacement for native WeakSet |
TrackedMap Example
I needed a map to cache user data by ID:
import { TrackedMap } from '@ember/reactive';
export default class UserCacheService extends Service { userCache = new TrackedMap();
getUser(id) { return this.userCache.get(id); }
setUser(id, user) { this.userCache.set(id, user); // Templates using getUser() will update }
removeUser(id) { this.userCache.delete(id); // Also triggers updates }}Any template that reads from this map will automatically re-render when items are added, updated, or deleted.
TrackedSet Example
For tracking unique selected items:
import { TrackedSet } from '@ember/reactive';
export default class ItemSelector extends Component { selectedItems = new TrackedSet();
toggleItem(item) { if (this.selectedItems.has(item)) { this.selectedItems.delete(item); } else { this.selectedItems.add(item); } }
isSelected(item) { return this.selectedItems.has(item); }}The template re-renders whenever items are added or removed from the set.
Common Mistakes I Made
Mistake 1: Mixing Native and Tracked Collections
I initially tried to convert an existing array to a TrackedArray:
// WRONG: Won't workconst nativeArray = [1, 2, 3];this.users = new TrackedArray(nativeArray); // Works for initial conversion
// Later, I accidentally assigned a native array againthis.users = nativeArray.filter(x => x > 1); // Lost tracking!The fix is to always use TrackedArray methods:
// CORRECT: Stay within TrackedArraythis.users = this.users.filter(x => x > 1); // Returns TrackedArrayMistake 2: Destructuring Breaks Tracking
I destructured methods from the collection, thinking it would be cleaner:
// WRONG: Loses tracking connectionconst { push } = this.users;push(newUser); // Template won't updateThe this context matters. Always call methods directly on the collection:
// CORRECT: Maintains trackingthis.users.push(newUser); // Template updatesMistake 3: Not Using the Constructor
I tried to assign a TrackedArray directly:
// WRONG: Creates a plain arraythis.users = TrackedArray; // Missing 'new' keyword!Always use the new keyword:
// CORRECTthis.users = new TrackedArray();// Or with initial valuesthis.users = new TrackedArray([1, 2, 3]);Classic Build Support
When Ember 6.8 first shipped, I couldn’t use @ember/reactive in my Classic ember-cli project. Importing the package would fail silently.
Ember 6.11 fixed this. The Classic AMD builds now properly expose the package, so it works across all build systems:
# Works in Vite-based builds (Ember 6.8+)# Works in Classic ember-cli builds (Ember 6.11+)npm install @ember/reactiveIf you’re on an older Ember version, upgrade to at least 6.11 for full compatibility.
Type Safety with TypeScript
The collections work well with TypeScript:
import { TrackedArray, TrackedMap } from '@ember/reactive';
interface User { id: string; name: string;}
export default class UserList extends Component { users: TrackedArray<User> = new TrackedArray<User>();
userMap: TrackedMap<string, User> = new TrackedMap<string, User>();
addUser(user: User) { this.users.push(user); this.userMap.set(user.id, user); }}Note the generic syntax uses < and > instead of < and > in MDX to avoid parsing issues.
Performance Considerations
I worried that tracked wrappers would add overhead. In practice, the performance impact is negligible for most applications.
The wrappers only add tracking notifications when mutations occur. Read operations like map.get() or array.find() have no overhead compared to native collections.
For extremely performance-sensitive code with thousands of mutations per frame (like a game or real-time visualization), you might want to batch updates manually. But for typical Ember applications, the convenience far outweighs any minimal overhead.
When to Use Each Collection
text title=“Collection Decision Guide” TrackedArray - Use for ordered lists with frequent mutations TrackedMap - Use for key-value lookups, caching by ID TrackedSet - Use for unique collections, selection state TrackedWeakMap - Use for metadata attached to objects (auto-cleanup) TrackedWeakSet - Use for object membership without preventing GC
Migration Checklist
If you have existing code with manual tracking workarounds:
- Identify collection properties with
@trackeddecorators - Replace native collections with tracked equivalents
- Remove unnecessary
@trackeddecorators (the collection handles tracking) - Update mutation code to use in-place methods (no more spread operators)
- Test that templates update correctly
In this post, I covered how @ember/reactive solves the collection tracking problem in Ember.js. I showed the common mistake of mutating native arrays and expecting reactivity, walked through the manual workarounds I tried first, and demonstrated how tracked collections provide a clean solution. The key insight is that native JavaScript collections don’t integrate with Glimmer’s autotracking, but TrackedArray, TrackedMap, and TrackedSet give you automatic reactivity without boilerplate.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments