Reactivity
Reactivity is the core of Blaze. When data changes, the UI updates automatically — you never manually manipulate the DOM.
How Reactivity Works
Blaze uses a computation-based reactive system:
- When a template renders, each
{{expression}}runs inside a computation - The computation tracks which reactive data sources are read
- When a reactive source changes, the computation re-runs
- Only the affected DOM nodes are updated
┌─────────────┐ reads ┌──────────────┐
│ Computation │──────────────▶│ ReactiveVar │
│ ({{count}}) │ │ count = 5 │
└──────┬───────┘ └──────┬───────┘
│ │
│ re-runs when │ .set(6)
│ dependency changes │
▼ │
┌─────────────┐ ┌───────┴──────┐
│ DOM update │ │ User code │
│ "5" → "6" │ │ or event │
└─────────────┘ └──────────────┘ReactiveVar
The most common reactive primitive:
import { ReactiveVar } from 'meteor/reactive-var';
const name = new ReactiveVar('Alice');
// Read (creates dependency in current computation)
name.get(); // 'Alice'
// Write (invalidates all dependent computations)
name.set('Bob');In Templates
Template.profile.onCreated(function () {
this.name = new ReactiveVar('Alice');
this.age = new ReactiveVar(30);
});
Template.profile.helpers({
name() {
return Template.instance().name.get();
},
age() {
return Template.instance().age.get();
},
isAdult() {
return Template.instance().age.get() >= 18;
},
});Autorun
Run code automatically whenever its dependencies change:
Template.dashboard.onCreated(function () {
this.filter = new ReactiveVar('all');
// Re-runs whenever filter changes
this.autorun(() => {
const filter = this.filter.get();
console.log('Filter changed to:', filter);
// Subscribe to new data
this.subscribe('todos', { filter });
});
});Reactive Computations in Helpers
Helpers are automatically reactive — they re-run when any reactive source they read changes:
Template.stats.helpers({
// Re-runs whenever the Todos collection changes
completedCount() {
return Todos.find({ completed: true }).count();
},
// Re-runs whenever completedCount or totalCount changes
completionRate() {
const total = Todos.find().count();
const completed = Todos.find({ completed: true }).count();
return total > 0 ? Math.round((completed / total) * 100) : 0;
},
});Fine-Grained Updates
Blaze only updates the exact DOM nodes that need to change:
When name changes, only the text inside <h1> is replaced. The <img>, .stats, and everything else stays untouched. This is why Blaze is so efficient — no virtual DOM diffing needed.
Non-Reactive Reads
Sometimes you want to read a reactive source without creating a dependency:
import { Blaze } from '@blaze-ng/core';
Template.myComponent.helpers({
initialValue() {
// Read without tracking — won't re-run when value changes
return Blaze.nonReactive(() => {
return Session.get('initialConfig');
});
},
});Custom Reactive Systems
Blaze-NG is framework-agnostic. You can use any reactive system:
Built-in SimpleReactiveSystem
For non-Meteor apps or testing:
import { Blaze } from '@blaze-ng/core';
import { SimpleReactiveSystem } from '@blaze-ng/core/testing';
const reactive = new SimpleReactiveSystem();
Blaze.setReactiveSystem(reactive);
// Create reactive variables
const name = reactive.ReactiveVar('Alice');
// Auto-run computations
reactive.autorun(() => {
console.log('Name is:', name.get());
});
name.set('Bob'); // logs: "Name is: Bob"
reactive.flush(); // Process pending updatesMeteor Tracker Adapter
import { createTrackerAdapter } from '@blaze-ng/meteor';
Blaze.setReactiveSystem(createTrackerAdapter(Tracker));Implement Your Own
Blaze.setReactiveSystem({
autorun(fn) {
// Run fn, track dependencies, re-run when they change
// Return a computation handle with { stop() }
},
nonReactive(fn) {
// Run fn without tracking
return fn();
},
ReactiveVar(initialValue) {
// Return { get(), set(value) }
let value = initialValue;
const listeners = new Set();
return {
get() {
/* track dependency, return value */
},
set(newValue) {
/* update value, notify listeners */
},
};
},
});Reactive Patterns
Derived State
Template.cart.onCreated(function () {
this.items = new ReactiveVar([]);
});
Template.cart.helpers({
items() {
return Template.instance().items.get();
},
// Computed from items — automatically updates
totalPrice() {
const items = Template.instance().items.get();
return items.reduce((sum, item) => sum + item.price * item.qty, 0);
},
itemCount() {
return Template.instance().items.get().length;
},
isEmpty() {
return Template.instance().items.get().length === 0;
},
});Debounced Search
Template.search.onCreated(function () {
this.query = new ReactiveVar('');
this.results = new ReactiveVar([]);
let timeout;
this.autorun(() => {
const query = this.query.get();
clearTimeout(timeout);
if (query.length < 2) {
this.results.set([]);
return;
}
// Debounce: wait 300ms after last keystroke
timeout = setTimeout(() => {
Meteor.call('search', query, (err, results) => {
if (!err) this.results.set(results);
});
}, 300);
});
});Loading States
Template.dataView.onCreated(function () {
this.isLoading = new ReactiveVar(true);
this.autorun(() => {
const handle = this.subscribe('myData');
this.isLoading.set(!handle.ready());
});
});