DEV Community

Cover image for The Magic of Reactivity and Data Binding in Native JavaScript
Omar Atta
Omar Atta

Posted on • Edited on • Originally published at oatta.codes

The Magic of Reactivity and Data Binding in Native JavaScript

Reactivity: The Secret Sauce of the Web

To put it simply, reactivity means that when you update a piece of data or a variable, every part of your webpage that uses or displays that data updates in real time. This automatic synchronization between the data and the UI elements ensures that your webpage always shows the most current information without needing manual refreshes or updates.

How Major Frameworks Handle Reactivity

In the land of web development, each team — whether it’s Team React, Team Vue, or Team Angular — plays the game a bit differently. React, for instance, has this cool trick up its sleeve called the Virtual DOM. It's like having a shadow copy of your web page that updates every time you make a change. This way, React knows exactly what needs to change on the real page, making updates smooth and fast.

Vue plays the game with a twist, using some smart moves to only change parts of the page that really need it, which means it can be quicker than React when it comes to making updates. React gives you the tools to make things speedy, too, but it's a bit like doing your own magic tricks — you've got to learn the ropes.

Then there's the new kid, Svelte, which kind of reminds us of the old-school way of doing things directly with the web page, like jQuery used to. But don’t worry, Svelte has some neat tricks to make it easier and more efficient, so it's not like going back to the stone age.

While all these tools are awesome for making web pages that respond to our every command, they sort of keep the magic under wraps. So, why not try making our own little reactivity spell with plain old JavaScript? It's like cooking your favorite dish from scratch — you get to see exactly what goes into it and appreciate the flavors even more. Let’s dive in and stir up some fun!

Diving Into Native JavaScript Reactivity

Ever had a friend who shares every tiny detail of their day? In JavaScript, that's what Observables are like. These digital chatterboxes keep your app informed about any data changes, acting as the behind-the-scenes heroes that ensure your application's data is as current as the latest news.

Understanding the Observable Pattern

The Observable pattern is like a newsletter subscription for your code. Just like you subscribe to get updates on your favorite topics, your code can "subscribe" to data it's interested in. Here’s the deal: an Observable is a data source, and it can send out updates to anyone who's subscribed. These subscribers are just parts of your code waiting eagerly, like fans at a concert, to react whenever their favorite band (or in this case, data) hits a new note.

Implementing an Observable Class

Roll up your sleeves; it's DIY time! First, you'll need a class that keeps track of who's subscribed to the newsletter (our Observable). Let's call it Observable. This class will have a list (or array) of subscribers and methods to add or remove subscribers, because, let’s face it, not everyone wants to hear about every single detail.

class Observable {
  #value;
  #subscribers = [];

  constructor(value) {
    this.#value = value;
  }

  get value() {
    return this.#value;
  }

  set value(newValue) {
    this.#value = newValue;
    this.notify();
  }

  subscribe(observer) {
    this.#subscribers.push(observer);
  }

  unsubscribe(observer) {
    this.#subscribers = this.#subscribers.filter(sub => sub !== observer);
  }

  notify() {
    this.#subscribers.forEach(subscriber => subscriber(this.#value));
  }
}
Enter fullscreen mode Exit fullscreen mode

#value and #subscribers are private fields, indicated by the # prefix. This means they can't be accessed or modified directly from outside the class.

The subscribe method is like adding a new friend to your group chat, unsubscribe is, well, when someone leaves the chat (it happens), and notify is sending out a message to everyone still in the chat. Whenever something noteworthy happens (like updating data), notify loops through all the subscribers and updates them with the new piece of info. It’s like saying, "Hey, listen up! Here’s what’s new!"

The value property holds the current state. The get method allows you to access this state, while the set method lets you update it. When the state changes via the setter, the Observable class notifies all subscribers about the update, ensuring everyone is informed about the latest state.

In plain JavaScript this can be used like the following:

const name = new Observable("Chanandler Bong");
name.subscribe((newName) => console.log(`Name changed to: ${newName}`));
name.value = "Chandler Bing!";
// Console Log: Name changed to: Chandler Bing!
Enter fullscreen mode Exit fullscreen mode

Computed Class: The Clever Cousin of Observables

A Computed class is essentially a special kind of observable that doesn't hold its own state in the traditional sense. Instead, its state is derived from other observables, the dependencies. Whenever any of these dependencies change, our Computed buddy goes, "Ah, something's new! Time to update my own value." It does this by re-running a function you give it, which computes its new value based on the latest states of its dependencies.

Let's code this out. Our Computed class will take a function and a list of observables (dependencies) in its constructor. It listens to these observables and updates its value by re-running the function whenever any dependency changes.

class Computed extends Observable {
  #dependencies;

  constructor(computeFunc, dependencies) {
    // Call the Observable constructor with the initial computed value
    super(computeFunc());
    this.#dependencies = dependencies;

    // Define listener to run on dependencies change
    const listener = () => {
      this.value = computeFunc();
    };

    // Subscribe to each dependency and run the listener
    this.#dependencies.forEach(dep => dep.subscribe(listener));
  }
}

Enter fullscreen mode Exit fullscreen mode

In this Computed class, we extend our Observable class because, well, a computed value is also observable! It can have subscribers that want to be notified when it changes, just like any regular observable.

The magic happens in the listner function, which recalculates the value whenever any dependency changes and updates this.value (inherited from Observable), triggering any subscriptions to be updated.

Doesn’t seem too difficult, right? Let’s see an example of how the computed class will be used.

Imagine a day at Central Perk where Chandler's jokes lighten the mood, but Ross's “WE WERE ON A BREAK” reminders of his break with Rachel bring it down. We'll use Observable instances for both Chandler's jokes and Ross's reminders, and a Computed instance to represent the overall mood.

const chandlerJokes = new Observable(0);
const rossBreakReminders = new Observable(0);

// A function to compute the Central Perk mood based on jokes and reminders
const computeMood = () => {
  if (chandlerJokes.value > rossBreakReminders.value) {
    return "Happy";
  } else if (chandlerJokes.value < rossBreakReminders.value) {
    return "Tense";
  }
  return "Neutral";
};

const centralPerkMood = new Computed(
    computeMood, 
    [chandlerJokes, rossBreakReminders]
);

// Function to log the mood
function logMood(mood) {
  console.log(`The mood in Central Perk is ${mood}.`);
}

// Subscribe to mood changes
centralPerkMood.subscribe(logMood);

// Simulate changes throughout the day
chandlerJokes.value += 3;
// Logs: The mood in Central Perk is Happy.

rossBreakReminders.value += 1;
// Logs: The mood in Central Perk is Happy.

rossBreakReminders.value += 3; // Ross brings up the break three more times
// Logs: The mood in Central Perk is Tense.

chandlerJokes.value += 1; // Chandler evens it out 4-4
// Logs: The mood in Central Perk is Neutral.
Enter fullscreen mode Exit fullscreen mode

Making Reactivity Work For You: Beyond the Console

We've mastered observables and computed values in the console, but the real magic unfolds on the screen where user interactions bring our code to life. Now, let's spotlight our Observable and Computed classes in a dynamic UI setting.

Imagine you're creating a shopping list app. Users can add items they need to buy, and the app displays the total number of items. As items are added or removed, the total updates in real-time. Sounds like a job for our observables!

HTML Setup

<input type="text" id="newItem">
<button id="addItem">Add Item</button>
<div>Total items: <span id="itemCountEl">0</span></div>
<ul id="itemListEl"></ul>
Enter fullscreen mode Exit fullscreen mode

First, we lay out our scene with a bit of HTML. We need an input field for new items, a button to add them, and a place to show the total count.

Casting Our Observables

const itemList = new Observable([]);
const itemCount = new Computed(() => itemList.value.length, [itemList]);
Enter fullscreen mode Exit fullscreen mode

Next, we introduce our data: itemList, an observable array to track the shopping items, and itemCount, a computed value that reflects the length of itemList.

Directing the Interaction

addItem.addEventListener('click', () => {
  if (newItem) {
    itemList.value = [...itemList.value, newItem.value]; // Add the new item
    newItem.value = ''; // Reset input field
  }
});

// Subscribe to update the UI whenever itemCount changes
itemCount.subscribe(count => {
  itemCountEl.textContent = count;
  itemListEl.innerHTML = itemList.value.map(item => `<li>${item}</li>`).join('');
});
Enter fullscreen mode Exit fullscreen mode

With our cast ready, it's time to direct the action. We need to update itemList when users add new items and ensure itemCount automatically reflects these changes on the screen.

Finally, as the curtain rises, our app springs to life. Users add items, and like a well-oiled machine, our observables and computed values ensure the UI stays perfectly in sync, updating the total item count in real-time. Here is the whole thing in action:

The Power of Proxy Objects

What's a Proxy?

A Proxy object wraps another object and intercepts the operations you perform on it, acting as a middleman. Think of it as having a personal assistant who filters your calls and messages, passing through only what you've asked to be notified about. This interception ability makes Proxy a perfect tool for creating reactive data structures.

Let's embark on a journey to implement reactivity using Proxy objects. Our goal? To create a reactive system where changes to our data automatically update the UI, without manually attaching listeners to every piece of data.

The Mechanics of Proxy Objects

A Proxy object requires two things to come to life: the target object you want to wrap and a handler object that defines the behavior (the set of actions you want to take) when interacting with the target. The handler object can specify a number of "traps," which are methods that provide property access control. These traps include getters for retrieving property values, setters for updating values, and many others for different operations.

  1. Target: This is the original object you want to wrap with a Proxy. It can be anything from an array to an object.
  2. Handler: This object defines the behavior of the Proxy. It contains traps for operations like reading a property (get) or writing to a property (set).

Creating a Proxy looks something like this:

const target = {}; // Your original object
const handler = {
  get(target, prop, receiver) {
    // Define behavior for reading a prop
  },
  set(target, prop, value, receiver) {
    // Define behavior for setting a prop
  },
  // Other traps...
};

const proxy = new Proxy(target, handler);
Enter fullscreen mode Exit fullscreen mode

With this setup, any action on proxy goes through the handler, allowing for controlled interaction with target.

Going Long: Example on the Game of Reactivity in JavaScript

Let's create a reactive example inspired by the iconic "Friends" Thanksgiving football game, where we'll track the scores of the two teams: "Team Monica" and "Team Ross." Using a Proxy object, we'll ensure that every time a team scores, the scoreboard is automatically updated.

UI prep

<div class="teams">
  <div class="team">
    <div class="name">Team Ross</div>
    <div class="score" id="teamRossScore">0</div>
  </div>
  <div class="team">
    <div class="name">Team Monica</div>
    <div class="score" id="teamMonicaScore">0</div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Setting Up the Game Scoreboard

const thanksgivingGameScores = {
  teamMonica: 0,
  teamRoss: 0
};
Enter fullscreen mode Exit fullscreen mode

Our handler will intercept score updates and ensure the scoreboard reflects these changes in real time.

function updateScoreboard(scores) {
  teamRossScore.textContent = scores.teamRoss;
  teamMonicaScore.textContent = scores.teamMonica;
};

const scoreHandler = {
  set(target, team, newScore) {
    console.log(`${team} scores! New score: ${newScore}`);
    target[team] = newScore; // Update the team's score

    // (Trap) Call to update the scoreboard UI
    updateScoreboard(target);

    return true; // Indicate successful score update
  }
};
Enter fullscreen mode Exit fullscreen mode

Wrapping the Scores with a Proxy

Next, we encapsulate our game scores within a Proxy to monitor and react to changes.

const reactiveScores = new Proxy(thanksgivingGameScores, scoreHandler);
Enter fullscreen mode Exit fullscreen mode

Playing the Game

Let’s simulate the game play:

setInterval(() => {
  // Decide randomly which team scores
  if (Math.random() < 0.5) {
    reactiveScores.teamMonica += 1;
  } else {
    reactiveScores.teamRoss += 1;
  }
}, 1000); // Update scores every 1 second

Enter fullscreen mode Exit fullscreen mode

As each team scores, our Proxy intercepts the updates, logs the score change, and calls updateScoreboard to refresh the displayed scores, keeping the audience engaged with the latest game developments. This example showcases the dynamic nature of Proxy objects in creating interactive and responsive web experiences. Here is the whole thing in action with some bad styling:

Key Takeaways on Reactivity: Proxy Objects and Observables

In wrapping up our exploration of reactivity in JavaScript, it's clear that both Proxy objects and Observables serve as foundational elements to grasp the broader concept of reactivity. Here's a succinct recap:

Proxy Objects offer a built-in way to intercept and manage interactions with objects, enabling automatic updates and notifications when data changes.

Observables provide a pattern for subscribing to data changes and broadcasting updates, crucial for keeping application state consistent.

These concepts, while not exhaustive in the realm of reactivity, lay the groundwork for understanding how dynamic updates can be achieved in web applications. They also offer insights into the mechanics behind some of the reactivity features in popular frameworks.

Understanding Proxy objects and Observables equips you with the basic tools to start seeing the underlying logic of reactivity in your favorite frameworks. This knowledge is not just theoretical; it's a stepping stone to implementing reactivity in your projects and possibly enhancing how you work with existing frameworks. Reactivity is a core principle in modern web development, and mastering these concepts is key to building responsive and intuitive applications.

⚠️ IMPORTANT ⚠️

Palestinian children are under attack. Call for ceasefire and consider donating to UNRWA or PCRF.

Top comments (0)