Tracker is an incredibly tiny (~1k) but incredibly powerful library for transparent reactive programming in JavaScript. It gives you much of the power of Functional Reactive Programming (FRP) without requiring you to rewrite your program as a data flow graph.
Tracker is essentially a simple convention that lets reactive data sources (like your database) talk to reactive data consumers (such as a live-updating HTML templating library) without the application code in between having to be involved.
With Tracker, you write normal JavaScript code, but it automatically reruns when data changes:
let temperature = 22;function displayTemp() { console.log(`Temperature: ${temperature}°C`);}displayTemp(); // Temperature: 22°Ctemperature = 23;// Nothing happens - need to call displayTemp() again
Here’s the complete temperature example from Tracker’s README:
import { Tracker } from 'meteor/tracker';// Simulate a thermometerclass Thermometer { constructor() { this._celsius = 22; this._dep = new Tracker.Dependency(); // Simulate temperature changes setInterval(() => { this._celsius += (Math.random() - 0.5) * 0.2; this._dep.changed(); }, 60000); } read() { this._dep.depend(); return this._celsius; }}const thermometer = new Thermometer();// Make it reactiveconst currentTemperatureCelsius = () => { return thermometer.read();};// Derived reactive functionconst currentTemperatureFahrenheit = () => { return currentTemperatureCelsius() * 9/5 + 32;};// Use it reactivelyconst handle = Tracker.autorun(() => { console.log( 'The current temperature is', currentTemperatureFahrenheit().toFixed(1), 'F' );});// Prints immediately:// The current temperature is 71.6 F// Prints again every minute when temperature changes
const computation = Tracker.autorun((comp) => { console.log('Running computation'); // Called on first run only if (comp.firstRun) { console.log('First run - setting up...'); } // Called when invalidated comp.onInvalidate(() => { console.log('About to rerun'); }); // Called when stopped comp.onStop(() => { console.log('Computation stopped - cleanup'); }); const data = ReactiveSource.get(); if (someCondition) { comp.stop(); }});
Tracker.autorun = function (f, options = {}) { if (typeof f !== 'function') throw new Error('Tracker.autorun requires a function argument'); constructingComputation = true; var c = new Tracker.Computation( f, Tracker.currentComputation, options.onError ); if (Tracker.active) Tracker.onInvalidate(function () { c.stop(); }); return c;};
// Good - one autorunTracker.autorun(() => { const user = Meteor.user(); const tasks = Tasks.find({ userId: user?._id }).fetch(); updateUI(user, tasks);});// Less optimal - two autorunsTracker.autorun(() => { const user = Meteor.user(); updateUserUI(user);});Tracker.autorun(() => { const tasks = Tasks.find().fetch(); updateTasksUI(tasks);});
Use Specific Queries
Reactive queries only invalidate when results change:
// Good - only reruns when completed tasks changeconst completed = Tasks.find({ completed: true }).count();// Less optimal - reruns on any task changeconst all = Tasks.find().fetch();const completed = all.filter(t => t.completed).length;