Understanding the Basic Example
Let's break down a simple todo list example step by step so you understand exactly how reactive collections work.
The scenario
You have a todo list where users can add tasks, mark them done, and see a count:
<input id="todoInput" placeholder="Add a task...">
<button id="addBtn">Add</button>
<ul id="todoList"></ul>
<span id="count">0 items</span>The code
// Step 1: Create the collection
const todos = Collections.create([
{ id: 1, text: 'Buy groceries', done: false },
{ id: 2, text: 'Walk the dog', done: false }
]);
// Step 2: Set up automatic UI updates
effect(() => {
Elements.todoList.update({ innerHTML: todos.items });
.map(t => `<li>${t.done ? '✅' : '⬜'} ${t.text}</li>`)
.join('');
Elements.count.update({ textContent: `${todos.length} items` });
});
// Step 3: Add a new todo
Elements.addBtn.addEventListener('click', () => {
todos.add({ id: Date.now(), text: Elements.todoInput.value, done: false });
Elements.todoInput.value = '';
});Let's break this down part by part.
Part 1: Creating the collection
const todos = Collections.create([
{ id: 1, text: 'Buy groceries', done: false },
{ id: 2, text: 'Walk the dog', done: false }
]);What happens inside
Collections.create([...])
↓
1️⃣ The initial items are copied into a new array
→ items: [{ id: 1, ... }, { id: 2, ... }]
↓
2️⃣ A reactive state is created: { items: [...] }
→ The items array is now tracked by the Proxy
↓
3️⃣ All collection methods are attached to the reactive object
→ .add(), .remove(), .update(), .toggle(), .clear(), etc.
↓
4️⃣ Getters are defined: .length, .first, .last
↓
5️⃣ The collection is returnedWhat the collection object looks like
console.log(todos.items); // [{ id: 1, text: 'Buy groceries', done: false }, ...]
console.log(todos.length); // 2
console.log(todos.first); // { id: 1, text: 'Buy groceries', done: false }
console.log(todos.last); // { id: 2, text: 'Walk the dog', done: false }It looks and behaves like a regular object, but with reactive superpowers and built-in methods.
Part 2: Setting up the effect
effect(() => {
Elements.todoList.update({ innerHTML: todos.items });
.map(t => `<li>${t.done ? '✅' : '⬜'} ${t.text}</li>`)
.join('');
Elements.count.update({ textContent: `${todos.length} items` });
});What happens
1️⃣ The effect runs immediately
↓
2️⃣ It reads todos.items → dependency tracked
↓
3️⃣ It reads todos.length (via the getter) → dependency tracked
↓
4️⃣ The DOM updates:
├── todoList shows:
│ ⬜ Buy groceries
│ ⬜ Walk the dog
└── count shows: "2 items"Now, any change to todos.items will re-run this effect and update the DOM.
Part 3: Adding an item
Elements.addBtn.addEventListener('click', () => {
todos.add({ id: Date.now(), text: Elements.todoInput.value, done: false });
Elements.todoInput.value = '';
});What happens when the button is clicked
todos.add({ id: 3, text: 'Clean house', done: false })
↓
1️⃣ .add() calls this.items.push(item)
→ Array now has 3 items
↓
2️⃣ The push triggers reactivity (via the array patch)
→ Effects depending on "items" are queued
↓
3️⃣ The effect re-runs
├── todoList shows:
│ ⬜ Buy groceries
│ ⬜ Walk the dog
│ ⬜ Clean house ← New item!
└── count shows: "3 items" ← Updated!
↓
4️⃣ .add() returns the collection (for chaining)The complete flow
Initial state:
┌──────────────────────────────┐
│ ⬜ Buy groceries │
│ ⬜ Walk the dog │
│ │
│ 2 items │
│ │
│ [____________] [Add] │
└──────────────────────────────┘After adding "Clean house":
┌──────────────────────────────┐
│ ⬜ Buy groceries │
│ ⬜ Walk the dog │
│ ⬜ Clean house │ ← New
│ │
│ 3 items │ ← Updated
│ │
│ [____________] [Add] │
└──────────────────────────────┘After removing "Walk the dog":
todos.remove(t => t.id === 2);┌──────────────────────────────┐
│ ⬜ Buy groceries │
│ ⬜ Clean house │
│ │
│ 2 items │ ← Updated
│ │
│ [____________] [Add] │
└──────────────────────────────┘After toggling "Buy groceries":
todos.toggle(t => t.id === 1, 'done');┌──────────────────────────────┐
│ ✅ Buy groceries │ ← Toggled!
│ ⬜ Clean house │
│ │
│ 2 items │
│ │
│ [____________] [Add] │
└──────────────────────────────┘Chaining in action
Multiple operations in one statement:
todos
.add({ id: 4, text: 'Read book', done: false })
.add({ id: 5, text: 'Exercise', done: false })
.sort((a, b) => a.text.localeCompare(b.text));Each method returns the collection, so you can chain the next call.
The two kinds of predicates
Most collection methods accept a predicate to find the right item. A predicate can be either:
1. A value — exact match
const numbers = Collections.create([1, 2, 3, 4, 5]);
numbers.remove(3); // Removes the number 3
numbers.includes(2); // true
numbers.indexOf(4); // position of 42. A function — conditional match
const todos = Collections.create([
{ id: 1, text: 'Buy milk', done: false },
{ id: 2, text: 'Clean house', done: true }
]);
todos.remove(t => t.id === 1); // Remove by condition
todos.find(t => t.done); // Find first done item
todos.update(t => t.id === 2, { text: 'Deep clean' }); // Update by conditionCommon beginner mistakes
❌ Mistake 1: Accessing items without .items
const list = Collections.create([1, 2, 3]);
// WRONG — the collection is not the array
console.log(list[0]); // undefined
console.log(list.length); // 3 (this works — it's a getter)
// RIGHT — access through .items
console.log(list.items[0]); // 1
console.log(list.at(0)); // 1 (or use .at())❌ Mistake 2: Forgetting that .items is the source of truth
// WRONG — this creates a copy, doesn't modify the collection
const copy = list.toArray();
copy.push(4); // Modifies the copy, not the collection
// RIGHT — use the collection methods
list.add(4); // Modifies the collection, triggers effects❌ Mistake 3: Forgetting the field name in toggle
const todos = Collections.create([
{ id: 1, text: 'Task', done: false }
]);
// WRONG — no field specified, defaults to 'done' (which may be fine)
todos.toggle(t => t.id === 1); // Toggles .done
// RIGHT when the field has a different name
todos.toggle(t => t.id === 1, 'active'); // Toggles .activeKey takeaways
- Create with
Collections.create(initialItems)orcollection(initialItems) - Data lives in
.items— an array tracked by the reactive system - Methods are chainable —
.add().remove().sort()all return the collection - Predicates accept a value (exact match) or a function (conditional match)
- Effects re-run whenever
.itemschanges through any method - Convenience getters —
.length,.first,.lastfor quick reads
What's next?
Let's explore every collection method in detail — CRUD operations, queries, iteration, and more.
Let's continue!