Master Reactivity in JavaScript: 8 Powerful Patterns…

Lokesh Prajapati
6 min readApr 15, 2024

In the realm of web development, reactivity plays a pivotal role. It embodies how a system dynamically responds to data alterations. This post delves into eight prominent JavaScript patterns that empower you to leverage reactivity effectively.

Why Embrace Reactivity?

As a front-end developer, you constantly grapple with an asynchronous environment within the browser. Modern web interfaces demand swift reactions to user interactions. This encompasses updating the UI, dispatching network requests, managing navigation, and a myriad of other tasks.

While frameworks are often synonymous with reactivity, a profound understanding can be cultivated by implementing these patterns in vanilla JavaScript. We’ll not only craft these patterns ourselves but also explore core browser APIs built upon the foundation of reactivity.

Table of Contents:

  • Pub/Sub: Simplified Data Flow Management
  • Custom Events: Browser-Built Pub/Sub for Easy Reactivity
  • Custom Event Targets: Focused Reactivity Control
  • Observer Pattern: Flexible Updates for Decoupled Code
  • Reactive Properties with Proxy: Track Changes on the Fly
  • Individual Property Reactivity: Fine-Grained Object Tracking
  • Reactive HTML Attributes with MutationObserver: DOM Changes Trigger Actions
  • Reactive Scrolling with IntersectionObserver: Visibility-Based Interactions

1. Pub/Sub: Simplified Data Flow Management

PubSub is one of the most commonly used and fundamental reactivity patterns. It establishes a communication channel between publishers (entities that disseminate data updates) and subscribers (entities that react to those updates).

Example:

class PubSub {
constructor() {
this.topics = {}; // Map event names to subscriber arrays
}
subscribe(topic, callback) {
if (!this.topics[topic]) {
this.topics[topic] = [];
}
this.topics[topic].push(callback);
}
publish(topic, data) {
if (this.topics[topic]) {
this.topics[topic].forEach((callback) => callback(data));
}
}
}

const pubsub = new PubSub();
pubsub.subscribe('news', (message) => console.log(`Received news: ${message}`));
pubsub.publish('news', 'Fresh headlines arrived!');

One popular example of its usage is Redux. This popular state management library is based on this pattern (or more specifically, the Flux architecture). Things work pretty simple in the context of Redux:

  • Publisher: The store acts as the publisher. When an action is dispatched, the store notifies all the subscribed components about the state change.
  • Subscriber: UI Components in the application are the subscribers. They subscribe to the Redux store and receive updates whenever the state changes.

2. Custom Events: Browser-Built Pub/Sub for Easy Reactivity

The browser offers an API for triggering and subscribing to custom events through the CustomEvent class and the dispatchEvent method. The latter provides us with the ability not only to trigger an event but also to attach any desired data to it.

Example:

const customEvent = new CustomEvent('customEvent', {
detail: 'Custom event data',
});

const element = document.getElementById('custom-event-target-element');
element.addEventListener('customEvent', (event) => console.log(`Received custom event: ${event.detail}`));
element.dispatchEvent(customEvent);

3. Custom Event Targets: Focused Reactivity Control

If you prefer not to dispatch events globally on the window object, you can create your own event target.

By extending the native EventTarget class, you can dispatch events to a new instance of it. This ensures that your events are triggered only on the new class itself, avoiding global propagation. Moreover, you have the flexibility to attach handlers directly to this specific instance.

Example:

class CustomEventTarget extends EventTarget {
constructor() {
super();
}
triggerCustomEvent(eventName, eventData) {
const event = new CustomEvent(eventName, { detail: eventData });
this.dispatchEvent(event);
}
}

const customTarget = new CustomEventTarget();
customTarget.addEventListener('customEvent', (event) => console.log(`Custom event received with data: ${event.detail}`));
customTarget.triggerCustomEvent('customEvent', 'Hello, This is a custom event!');

4. Observer Pattern: Flexible Updates for Decoupled Code

The Observer pattern is really similar to PubSub. You subscribe to the Subject and then it notifies its subscribers (Observers) about changes, allowing them to react accordingly. This pattern plays a significant role in building decoupled and flexible architecture.

Example:

class Subject {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
const index = this.observers.indexOf(observer);
if (index !== -1) {
this.observers.splice(index, 1);
}
}
notify(data) {
this.observers.forEach((observer) => observer.update(data));
}
}

class Observer {
constructor(name) {
this.name = name;
}
update(data) {
console.log(`${this.name} received an update: ${data}`);
}
}

const subject = new Subject();
const observer1 = new Observer('Observer 1');
const observer2 = new Observer('Observer 2');

// Add observers to the subject
subject.addObserver(observer1);
subject.addObserver(observer2);

// Notify observers about changes
subject.notify();

// console logs are:
// Observer 1 received an update.
// Observer 2 received an update.

5. Reactive Properties with Proxy: Track Changes on the Fly

The Proxy object grants you the power to intercept property access operations (get and set) within objects. This enables you to implement reactivity and execute code whenever a property’s value is retrieved or modified.

Example:

let data = {
name: 'Lokesh',
age: 25,
};

let reactiveData = new Proxy(data, {
get(target, property) {
console.log(`Property "${property}" has been read, current value: ${target[property]}`);
return target[property];
},
set(target, property, value) {
if (value !== target[property]) { // Check for actual change before logging
console.log(`Property "${property}" changed from ${target[property]} to ${value}`);
target[property] = value;
// Additional actions can be triggered here, such as updating the UI
console.log(`Updated property "${property}" to ${value}`);
}
return true; // Indicates that the property was successfully set
},
});

// Example functions to interact with the reactive data
function updateName(newName) {
reactiveData.name = newName;
}

function readName() {
console.log(`The name is: ${reactiveData.name}`);
}

// Simulating property updates and reads
updateName('Rajesh');
readName();

6. Individual Property Reactivity: Fine-Grained Object Tracking

If you don’t need to track all the fields in the objects, you can choose the specific one using Object.defineProperty or group of them with Object.defineProperties.

Example:

let user = { _name: 'Lokesh' };

Object.defineProperty(user, 'name', {
get() {
console.log(`Getting name: ${this._name}`);
return this._name;
},
set(value) {
console.log(`Setting name from ${this._name} to ${value}`);
this._name = value;
},
enumerable: true, // makes the property show up during enumeration
configurable: true // allows the property to be deleted or changed
});

console.log(user.name); // Accessing the name, triggers getter
user.name = 'Rajesh'; // Changing the name, triggers setter
console.log(user.name); // Accessing the name again, triggers getter

7. Reactive HTML Attributes with MutationObserver: DOM Changes Trigger Actions

MutationObserver is a JavaScript API that allows you to observe changes to the DOM (Document Object Model), such as changes in attributes, additions or removals of elements, and more. It is especially useful for running code in response to DOM modifications without needing to manually track those changes.

Example:

<div id="observed-element">Watch me for changes!</div>
<button onclick="toggleClass()">Toggle Class</button>
// Select the element to observe
const observedElement = document.getElementById('observed-element');

// Define a function to toggle a class
function toggleClass() {
observedElement.classList.toggle('highlight');
}

// Callback function to execute when mutations are observed
const callback = (mutationsList, observer) => {
for (const mutation of mutationsList) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
console.log(`Class attribute changed!`);
// You can add more actions here, e.g., change style, log more details, etc.
if (observedElement.classList.contains('highlight')) {
observedElement.style.backgroundColor = 'yellow';
} else {
observedElement.style.backgroundColor = '';
}
}
}
};

// Create a new MutationObserver and pass the callback function
const observer = new MutationObserver(callback);

// Set the configuration: observe attribute changes
const config = { attributes: true };

// Start observing the specified element
observer.observe(observedElement, config);

8. Reactive Scrolling with IntersectionObserver: Visibility-Based Interactions

IntersectionObserver is an API that provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport. This means you can execute code when an element enters or exits the viewport or another specific element.

This is particularly useful for lazy loading images, infinite scrolling, animations based on scroll position, and more.

Example:

<div id="special-element" style="height: 500px; background-color: grey; margin-top: 1000px;">
I change color when visible on screen!
</div>
// Function to be called when the observed element is intersecting
function handleIntersection(entries, observer) {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.backgroundColor = 'lightgreen';
} else {
entry.target.style.backgroundColor = 'grey';
}
});
}

// Create an IntersectionObserver with the callback
const observer = new IntersectionObserver(handleIntersection);

// Start observing an element
const specialElement = document.getElementById('special-element');
observer.observe(specialElement);

Thanks for reading!

I hope you found this article useful. If you have any questions or suggestions, please leave comments. Your feedback helps me to become better.

Don’t forget to subscribe⭐

--

--