Skip to content

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.

components/user-list.js
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:

components/user-list.js
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:

components/user-list.js
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.

components/user-list.js
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:

CollectionDescription
TrackedArrayDrop-in replacement for native Array
TrackedMapDrop-in replacement for native Map
TrackedSetDrop-in replacement for native Set
TrackedWeakMapDrop-in replacement for native WeakMap
TrackedWeakSetDrop-in replacement for native WeakSet

TrackedMap Example

I needed a map to cache user data by ID:

services/user-cache.js
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:

components/item-selector.js
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:

components/user-list.js
// WRONG: Won't work
const nativeArray = [1, 2, 3];
this.users = new TrackedArray(nativeArray); // Works for initial conversion
// Later, I accidentally assigned a native array again
this.users = nativeArray.filter(x => x > 1); // Lost tracking!

The fix is to always use TrackedArray methods:

components/user-list.js
// CORRECT: Stay within TrackedArray
this.users = this.users.filter(x => x > 1); // Returns TrackedArray

Mistake 2: Destructuring Breaks Tracking

I destructured methods from the collection, thinking it would be cleaner:

components/user-list.js
// WRONG: Loses tracking connection
const { push } = this.users;
push(newUser); // Template won't update

The this context matters. Always call methods directly on the collection:

components/user-list.js
// CORRECT: Maintains tracking
this.users.push(newUser); // Template updates

Mistake 3: Not Using the Constructor

I tried to assign a TrackedArray directly:

components/user-list.js
// WRONG: Creates a plain array
this.users = TrackedArray; // Missing 'new' keyword!

Always use the new keyword:

components/user-list.js
// CORRECT
this.users = new TrackedArray();
// Or with initial values
this.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:

terminal output
# Works in Vite-based builds (Ember 6.8+)
# Works in Classic ember-cli builds (Ember 6.11+)
npm install @ember/reactive

If 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:

components/user-list.ts
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 &lt; and &gt; 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:

  1. Identify collection properties with @tracked decorators
  2. Replace native collections with tracked equivalents
  3. Remove unnecessary @tracked decorators (the collection handles tracking)
  4. Update mutation code to use in-place methods (no more spread operators)
  5. 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