Lists and Iteration
Render collections of data reactively with {{#each}}.
Basic {{#each}}
ts
Template.fruitList.helpers({
fruits() {
return ['Apple', 'Banana', 'Cherry'];
},
});Iterating Objects
When iterating an array of objects, each object becomes the data context:
ts
Template.userList.helpers({
users() {
return [
{ name: 'Alice', email: 'alice@example.com', avatar: '/avatars/alice.jpg' },
{ name: 'Bob', email: 'bob@example.com', avatar: '/avatars/bob.jpg' },
];
},
});Named Iteration — {{#each item in list}}
Avoids data context issues and makes code clearer:
This is especially useful when nesting:
Empty State with {{else}}
Show a fallback when the list is empty:
Reactive Cursors
{{#each}} works seamlessly with reactive data sources. When the underlying data changes, only the affected DOM elements are updated:
ts
Template.taskList.helpers({
tasks() {
// Returns a reactive cursor — UI updates automatically
return Tasks.find({ listId: this.listId }, { sort: { createdAt: -1 } });
},
});How Reactive Updates Work
When an item is:
- Added → Only the new element is inserted
- Removed → Only that element is removed
- Moved → The element is repositioned (no re-render)
- Changed → Only that element's content updates
This means hundreds of items can update efficiently without re-rendering the entire list.
Nested Iteration
Index Access
Use @index to get the zero-based index of the current item:
Filtering and Sorting
Use helpers to transform data before rendering:
ts
Template.productList.helpers({
filteredProducts() {
const filter = Template.instance().filter.get();
const sort = Template.instance().sort.get();
const query = {};
if (filter.category) query.category = filter.category;
if (filter.minPrice) query.price = { $gte: filter.minPrice };
if (filter.search) {
query.name = { $regex: filter.search, $options: 'i' };
}
return Products.find(query, {
sort: { [sort.field]: sort.direction },
limit: 50,
});
},
categories() {
return _.uniq(Products.find().map((p) => p.category)).sort();
},
});Performance Tips
Use _id for Efficient Diffing
When iterating database documents, Blaze uses _id to track items. This enables efficient DOM updates:
ts
// Good — returns cursor, Blaze uses _id for tracking
Template.list.helpers({
items() {
return Items.find();
},
});
// Also good — array with _id fields
Template.list.helpers({
items() {
return [
{ _id: '1', name: 'First' },
{ _id: '2', name: 'Second' },
];
},
});Limit Rendered Items
For large lists, paginate or virtualize:
ts
Template.infiniteList.onCreated(function () {
this.limit = new ReactiveVar(20);
});
Template.infiniteList.helpers({
items() {
return Items.find(
{},
{
sort: { createdAt: -1 },
limit: Template.instance().limit.get(),
},
);
},
hasMore() {
return Items.find().count() > Template.instance().limit.get();
},
});
Template.infiniteList.events({
'click .load-more'(event, instance) {
instance.limit.set(instance.limit.get() + 20);
},
});Avoid Heavy Helpers in Loops
Complete Example: Sortable Table
ts
Template.dataTable.onCreated(function () {
this.sortField = new ReactiveVar('name');
this.sortDirection = new ReactiveVar('asc');
this.limit = new ReactiveVar(25);
});
Template.dataTable.helpers({
columns: () => [
{ field: 'name', label: 'Name' },
{ field: 'email', label: 'Email' },
{ field: 'role', label: 'Role' },
{ field: 'createdAt', label: 'Joined' },
],
rows() {
const instance = Template.instance();
const sortField = instance.sortField.get();
const direction = instance.sortDirection.get() === 'asc' ? 1 : -1;
return Users.find(
{},
{
sort: { [sortField]: direction },
limit: instance.limit.get(),
},
);
},
sortField() {
return Template.instance().sortField.get();
},
sortDirection() {
return Template.instance().sortDirection.get();
},
totalCount() {
return Users.find().count();
},
hasMore() {
return Users.find().count() > Template.instance().limit.get();
},
});
Template.dataTable.events({
'click th[data-field]'(event, instance) {
const field = event.currentTarget.dataset.field;
if (instance.sortField.get() === field) {
instance.sortDirection.set(instance.sortDirection.get() === 'asc' ? 'desc' : 'asc');
} else {
instance.sortField.set(field);
instance.sortDirection.set('asc');
}
},
'click .load-more'(event, instance) {
instance.limit.set(instance.limit.get() + 25);
},
});