--- title: Use Reactive Collections from @ember/reactive/collections impact: HIGH impactDescription: Enables reactive arrays, maps, and sets tags: 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):** ```glimmer-js // 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); } } ``` **Correct (reactive array with @ember/reactive/collections):** ```glimmer-js // 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); } } ``` ### Reactive Maps Maps are useful for key-value stores with non-string keys: ```glimmer-js // 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()); } } ``` ### Reactive Sets Sets are useful for unique collections: ```glimmer-js // 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; } } ``` ### 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:** ```javascript 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:** ```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:** ```javascript 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: ```javascript 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 1. **Don't mix approaches** - choose either reactive collections or immutable updates 2. **Initialize in class field** - no need for constructor 3. **Use appropriate type** - Map for key-value, Set for unique values, Array for ordered lists 4. **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:** - [Ember Reactivity System](https://guides.emberjs.com/release/in-depth-topics/autotracking-in-depth/) - [JavaScript Built-in Objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects) - [Reactive Collections RFC](https://github.com/emberjs/rfcs/blob/master/text/0869-reactive-collections.md)