Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/meteor/meteor/llms.txt

Use this file to discover all available pages before exploring further.

ReactiveVar provides a single reactive value that can be get and set, automatically invalidating computations that depend on it.

Installation

meteor add reactive-var

Overview

ReactiveVar is:
  • A container for a single reactive value
  • Similar to Session variables but scoped (not global)
  • More flexible than Session (can hold any value, not just EJSON)
  • Integrated with Tracker for automatic reactivity
  • Efficient at absorbing unnecessary invalidations

Package Information

  • Version: 1.0.13
  • Summary: Reactive variable
  • Dependencies: tracker
  • Export: ReactiveVar

Basic Usage

Creating a ReactiveVar

import { ReactiveVar } from 'meteor/reactive-var';

// Create with initial value
const counter = new ReactiveVar(0);

const message = new ReactiveVar('Hello');

const user = new ReactiveVar(null);

const settings = new ReactiveVar({ theme: 'dark', lang: 'en' });

Getting Values

// Get current value (reactive)
const value = counter.get();

console.log('Counter:', counter.get());
// 0

Setting Values

// Set new value
counter.set(5);

message.set('Hello, World!');

user.set({ name: 'Alice', id: 123 });

Reactive Updates

ReactiveVar automatically invalidates computations:
const count = new ReactiveVar(0);

// This computation depends on count
Tracker.autorun(() => {
  console.log('Count is:', count.get());
});
// Prints: Count is: 0

count.set(1);
// Prints: Count is: 1

count.set(2);
// Prints: Count is: 2

Constructor

new ReactiveVar(initialValue, equalsFunc)
initialValue
any
required
The initial value to set
equalsFunc
function
Optional function to determine if values are equal. Called with (oldValue, newValue). If it returns true, no invalidation occurs.

Custom Equality Function

By default, ReactiveVar uses a conservative equality check:
// Default behavior: object changes always trigger updates
const obj = new ReactiveVar({ x: 1 });

Tracker.autorun(() => {
  console.log('Object:', obj.get());
});

obj.set({ x: 1 }); // Triggers update even though value is same

// Custom equality: deep comparison
const deepObj = new ReactiveVar({ x: 1 }, (a, b) => {
  return JSON.stringify(a) === JSON.stringify(b);
});

Tracker.autorun(() => {
  console.log('Deep object:', deepObj.get());
});

deepObj.set({ x: 1 }); // No update - values are equal
deepObj.set({ x: 2 }); // Updates - values differ

Methods

get()

Returns the current value and registers a dependency:
const value = myVar.get();
Calling get() inside a reactive computation (like Tracker.autorun) establishes a dependency.

set(newValue)

Sets a new value and invalidates dependents if changed:
myVar.set(newValue);
newValue
any
required
The new value to set

Default Equality

The default equality function (ReactiveVar._isEqual):
// These are considered equal:
var1.set(42);
var1.set(42); // No invalidation

var2.set(true);
var2.set(true); // No invalidation

var3.set('hello');
var3.set('hello'); // No invalidation

var4.set(null);
var4.set(null); // No invalidation

// These always trigger invalidations:
var5.set({ x: 1 });
var5.set({ x: 1 }); // Invalidates! Objects are not deep-compared

var6.set([1, 2, 3]);
var6.set([1, 2, 3]); // Invalidates! Arrays are not deep-compared
The default check only considers primitive values equal:
  • number
  • boolean
  • string
  • undefined
  • null

Patterns and Examples

Pattern: Component State

class Counter {
  constructor() {
    this.count = new ReactiveVar(0);
  }
  
  increment() {
    this.count.set(this.count.get() + 1);
  }
  
  decrement() {
    this.count.set(this.count.get() - 1);
  }
  
  reset() {
    this.count.set(0);
  }
}

const counter = new Counter();

Tracker.autorun(() => {
  console.log('Count:', counter.count.get());
});

counter.increment(); // Prints: Count: 1
counter.increment(); // Prints: Count: 2
counter.reset();     // Prints: Count: 0

Pattern: Loading State

const isLoading = new ReactiveVar(false);

async function fetchData() {
  isLoading.set(true);
  
  try {
    const data = await fetch('/api/data');
    return data;
  } finally {
    isLoading.set(false);
  }
}

// In template helper or React component
Tracker.autorun(() => {
  if (isLoading.get()) {
    showLoadingSpinner();
  } else {
    hideLoadingSpinner();
  }
});

Pattern: Form State

class FormState {
  constructor() {
    this.username = new ReactiveVar('');
    this.email = new ReactiveVar('');
    this.password = new ReactiveVar('');
  }
  
  isValid() {
    return (
      this.username.get().length >= 3 &&
      this.email.get().includes('@') &&
      this.password.get().length >= 8
    );
  }
  
  clear() {
    this.username.set('');
    this.email.set('');
    this.password.set('');
  }
  
  getData() {
    return {
      username: this.username.get(),
      email: this.email.get(),
      password: this.password.get()
    };
  }
}

const form = new FormState();

// Reactive validation
Tracker.autorun(() => {
  const valid = form.isValid();
  submitButton.disabled = !valid;
});

Pattern: Derived ReactiveVars

const firstName = new ReactiveVar('John');
const lastName = new ReactiveVar('Doe');

// Computed value (not a ReactiveVar, but reactive)
function fullName() {
  return `${firstName.get()} ${lastName.get()}`;
}

Tracker.autorun(() => {
  console.log('Full name:', fullName());
});
// Prints: Full name: John Doe

firstName.set('Jane');
// Prints: Full name: Jane Doe

// Or create a ReactiveVar that depends on others
const fullNameVar = new ReactiveVar('');

Tracker.autorun(() => {
  fullNameVar.set(`${firstName.get()} ${lastName.get()}`);
});

Pattern: Toggle State

class Toggle {
  constructor(initialState = false) {
    this._state = new ReactiveVar(initialState);
  }
  
  get() {
    return this._state.get();
  }
  
  set(value) {
    this._state.set(!!value);
  }
  
  toggle() {
    this._state.set(!this._state.get());
  }
  
  on() {
    this._state.set(true);
  }
  
  off() {
    this._state.set(false);
  }
}

const darkMode = new Toggle(false);

Tracker.autorun(() => {
  document.body.className = darkMode.get() ? 'dark' : 'light';
});

darkMode.toggle(); // Switches to dark mode

Pattern: Array Modifications

Since arrays are objects, you need to handle them specially:
// This doesn't work as expected:
const items = new ReactiveVar([]);
items.get().push('new item'); // No invalidation!

// Correct approach:
function addItem(item) {
  const current = items.get();
  items.set([...current, item]); // Creates new array
}

function removeItem(index) {
  const current = items.get();
  items.set(current.filter((_, i) => i !== index));
}

// Or use custom equality:
const items = new ReactiveVar([], (a, b) => {
  // Always consider different (update on every set)
  return false;
});

Comparison with Session

ReactiveVar

const myVar = new ReactiveVar(0);
myVar.set(5);
const value = myVar.get();

// ✓ Scoped (not global)
// ✓ Can hold any value (functions, DOM nodes, etc.)
// ✓ Must be imported and passed around
// ✓ Not persisted across hot code push
// ✓ Custom equality functions

Session

Session.set('myVar', 0);
Session.set('myVar', 5);
const value = Session.get('myVar');

// ✓ Global (accessible anywhere)
// ✓ Only EJSON-able values
// ✓ Available everywhere by default
// ✓ Persisted across hot code push
// ✓ String keys can collide

Using with React

import { ReactiveVar } from 'meteor/reactive-var';
import { useTracker } from 'meteor/react-meteor-data';

const counter = new ReactiveVar(0);

function Counter() {
  const count = useTracker(() => counter.get(), []);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => counter.set(count + 1)}>
        Increment
      </button>
    </div>
  );
}

Using with Blaze

// In template
Template.myTemplate.onCreated(function() {
  this.counter = new ReactiveVar(0);
});

Template.myTemplate.helpers({
  count() {
    return Template.instance().counter.get();
  }
});

Template.myTemplate.events({
  'click .increment'(event, instance) {
    const current = instance.counter.get();
    instance.counter.set(current + 1);
  }
});
<template name="myTemplate">
  <p>Count: {{count}}</p>
  <button class="increment">Increment</button>
</template>

Performance Considerations

ReactiveVar is lightweight - Very little overhead compared to Session
Absorbs redundant updates - Setting the same primitive value multiple times doesn’t cause reruns
Object/Array updates - Always trigger invalidations unless using custom equality
// Efficient for primitives
const count = new ReactiveVar(0);
count.set(0); // No invalidation
count.set(0); // No invalidation
count.set(1); // Invalidates

// Inefficient for objects without custom equality
const obj = new ReactiveVar({ x: 1 });
obj.set({ x: 1 }); // Invalidates
obj.set({ x: 1 }); // Invalidates again!

// Better: use custom equality or immutable updates
const obj2 = new ReactiveVar(
  { x: 1 },
  (a, b) => a.x === b.x
);
obj2.set({ x: 1 }); // No invalidation
obj2.set({ x: 2 }); // Invalidates

Debugging

const myVar = new ReactiveVar(0);

// Track all changes
const originalSet = myVar.set.bind(myVar);
myVar.set = function(newValue) {
  console.log('Setting value:', newValue);
  console.trace('Called from:');
  return originalSet(newValue);
};

// Count dependents (internal API)
console.log('Dependents:', myVar._numListeners());

reactive-dict

Reactive key-value dictionaries

tracker

Reactive computation system

session

Global reactive state

Source Code