Skip to content

JavaScript Reactivity for Beginners

Let’s start with the simplest form of data storage in JavaScript:

let price = 10;
let quantity = 2;
let total = price * quantity;
console.log(total); // 20
// If we change price
price = 20;
console.log(total); // Still 20! Not what we want

The problem: When we change price, total doesn’t automatically update. This is because JavaScript is not reactive by default.

The simplest way to make something reactive is to use functions:

let price = 10;
let quantity = 2;
function calculateTotal() {
return price * quantity;
}
console.log(calculateTotal()); // 20
price = 20;
console.log(calculateTotal()); // 40 - Now it updates!

This works, but we have to manually call calculateTotal() every time we want the updated value.

3. Using Object Properties (Getters and Setters)

Section titled “3. Using Object Properties (Getters and Setters)”

Object.defineProperty is a built-in JavaScript method that allows you to define or modify the properties of an object. It takes three arguments: the object on which you want to define the property, the name of the property, and an object that contains the property’s descriptors.

Here’s how it works:

let data = {
price: 10,
quantity: 2,
};
let total = 0;
// Create a reactive property
Object.defineProperty(data, "total", {
get() {
return this.price * this.quantity;
},
});
console.log(data.total); // 20
data.price = 20;
console.log(Object.keys(data)); // ['price', 'quantity'] - The `Object.keys()` method returns an array of a given object's own enumerable property names. In this case, we're logging the keys of the `data` object, which includes the `price` and `quantity` properties.
console.log(Object.keys(data)); // ['price', 'quantity'] - The `total` property is not included in the array of keys. The `Object.keys()` method returns an array of a given object's own enumerable property names, but it does not include properties created by getters or setters. In this case, the `total` property is a getter that calculates the total based on the `price` and `quantity` properties, so it is not included in the array of keys.

The Proxy object is a more modern way to make objects reactive:

let data = {
price: 10,
quantity: 2,
};
// Create a proxy
let proxy = new Proxy(data, {
get(target, property) {
console.log(`Getting ${property}`);
return target[property];
},
set(target, property, value) {
console.log(`Setting ${property} to ${value}`);
target[property] = value;
return true;
},
});
console.log(proxy.price); // Logs: "Getting price" then 10
proxy.price = 20; // Logs: "Setting price to 20"

Let’s build a basic reactivity system step by step:

// 1. Create a dependency tracker
class Dep {
constructor() {
this.subscribers = new Set();
}
// Add a subscriber
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect);
}
}
// Notify all subscribers
notify() {
this.subscribers.forEach((effect) => effect());
}
}
// 2. Create a reactive function
let activeEffect = null;
function watchEffect(effect) {
activeEffect = effect;
effect();
activeEffect = null;
}
// 3. Make an object reactive
function reactive(obj) {
const deps = new Map();
return new Proxy(obj, {
get(target, key) {
if (activeEffect) {
let dep = deps.get(key);
if (!dep) {
dep = new Dep();
deps.set(key, dep);
}
dep.depend();
}
return target[key];
},
set(target, key, value) {
target[key] = value;
const dep = deps.get(key);
if (dep) {
dep.notify();
}
return true;
},
});
}
// 4. Use our reactive system
const state = reactive({
price: 10,
quantity: 2,
});
// Watch for changes
watchEffect(() => {
console.log(`Total: ${state.price * state.quantity}`);
});
// Change values
state.price = 20; // Logs: "Total: 40"
state.quantity = 3; // Logs: "Total: 60"

Let’s see how this works in a practical example:

// Create a reactive shopping cart
const cart = reactive({
items: [],
total: 0,
});
// Watch for changes in the cart
watchEffect(() => {
cart.total = cart.items.reduce((sum, item) => sum + item.price, 0);
console.log(`Cart total: $${cart.total}`);
});
// Add items to cart
cart.items.push({ name: "Apple", price: 1 });
cart.items.push({ name: "Banana", price: 2 });
// Logs: "Cart total: $3"
// Update item price
cart.items[0].price = 2;
// Logs: "Cart total: $4"
const state = reactive({
count: 0,
});
watchEffect(() => {
document.getElementById("counter").textContent = state.count;
});
// Update will automatically reflect in the DOM
state.count++;
let state = {
count: 0,
};
function setState(newState) {
state = { ...state, ...newState };
render();
}
function render() {
document.getElementById("counter").textContent = state.count;
}
// Update will trigger a re-render
setState({ count: state.count + 1 });
  1. Reactivity means automatically updating when data changes
  2. Dependencies are tracked when data is accessed
  3. Effects are functions that run when dependencies change
  4. Proxies and Getters/Setters are tools to make objects reactive
  5. State Management is about keeping track of changes
  1. Circular Dependencies
// Avoid this!
watchEffect(() => {
state.a = state.b + 1;
});
watchEffect(() => {
state.b = state.a + 1;
});
  1. Memory Leaks
// Always clean up effects when done
const stop = watchEffect(() => {
// effect code
});
// When component unmounts
stop();