Skip to content

Data Binding with Reflex

Example of Reflex clock and counter

Introduction

By now, you’ve created interactive web applications and should be familiar with getting DOM elements, adding event listeners, and updating the DOM in response to user interactions!

The following pattern should be very familiar to you:

let counter = 0; // Tracks the "state" of the counter

// 1. Get DOM element(s)
const counterDisplay = document.getElementById("counter-display");
const incrementButton = document.getElementById("increment-button");

// 2. Add event listener
incrementButton.addEventListener("click", () => {
  counter++;

  // 3. Update the DOM element(s) to reflect the new state
  counterDisplay.textContent = `Counter: ${counter}`;
});

Let’s dive deeper into this pattern and see how modern user interface (UI) frameworks handle this for us with a powerful concept called data binding.

Learning Objectives

By the end of this activity, you should be able to:

  • Identify the difficulties with manual DOM manipulation
  • Understand the concept of data binding and how it helps with UI development
  • Understand the underlying implementation of Reflex and how it achieves data binding
  • Use Reflex to create state variables and bind them to the UI for automatic updates

Starter Code

Download the starter code and unzip it in a safe location (e.g., your cs343 folder):

reflex.zip

Open the folder in VS Code.

Part 1: DOM Manipulation

(20 minutes)

Let’s start by implementing some interactive features using DOM manipulation, just like we’ve done in previous activities. (It’s good practice!)

Overview

Open up interactivity.html in VS Code.

You’ll see four sections in the HTML, describing four separate interactivity examples:

  • 🕑 A clock that should update every second
  • 🔢 A counter that should increment and decrement when you click the buttons
  • 🎨 A color picker that should change the background color of the page
  • 📃 A form that should make a request and display the response

Your goal is to implement all four examples.

Instructions

  1. For the clock, you can create a new Date() to get the current date/time
  2. For the counter example, you’ll need three event listeners
    • All three should update the counter’s value and the counter display
  3. For the color picker, you can use the input event to detect when the user changes the color
    • The value of the color picker is a hex code like "#ff0000" that you can set as the page’s background color (e.g., document.body.style.backgroundColor = "#ff0000")
    • Don’t forget to update the color code display too!
  4. The form should send the name and age values in the request body and display the response
    • You should pretty-print the response JSON before displaying it (e.g., JSON.stringify(data, null, 2))

Don’t forget to test your code by opening interactivity.html in a preview and trying out the different features!

Part 2: Reflex

(20 minutes)

First of all, let’s think about what made that annoying to implement. Take a moment to discuss with a partner, then expand the section below to see our thoughts:

Think: What were some of the pain points?
  • You had to retrieve the DOM elements using document.getElementById or similar
    • All of those elements needed unique ids, which can be a pain to manage
  • You had to manually add event listeners to the buttons and inputs
    • Just looking at the HTML, you can’t immediately tell where the code for each element resides
  • Changing the value of counter didn’t automatically update the display; you had to remember to update the DOM every time you changed the state
    • If you forgot to update the display, the UI would be out of sync with the counter!

There’s gotta be a better way!

Data Binding

Modern UI frameworks like React, Vue, and Svelte all use a powerful concept called data binding to solve these problems. There are two main types of data bindings that we’ll talk about:

Diagram of data binding. One-way Binding: State -> UI. Two-way Binding: State <-> UI

  • One-way: When the state changes, the UI automatically updates to reflect the new state.
  • Two-way: When the state changes, the UI automatically updates, and when the user interacts with the UI (e.g., typing in an input), it likewise updates the state.

One-way bindings are used for things like displaying data, while two-way bindings are used for inputs and controls where the user needs to update the state from the UI.

Data binding allows a UI to be “reactive”. You no longer need to manually update the UI when your state changes; instead, you can just declare your state variables and bind them to the UI, and the framework will automatically keep everything in sync for you!

Overview

Now, open up reflex.html and reflex.js in VS Code.

The HTML file contains the same four examples as before. This time, you’ll implement them using Reflex, which is a simple UI framework that provides data binding.

We’ve given you some starter code for the clock example. Let’s isolate it and take a closer look:

<h3>Time:</h3>
<div data-binding="currentTime"></div>

<script>
  const timeState = new State("currentTime", new Date().toLocaleTimeString());
</script>

In the JavaScript, we create a new State object and assign it to timeState:

  • The first argument is the name of the state variable: "currentTime"
  • The second argument is the initial value of the state variable: new Date().toLocaleTimeString()

Take a look at the HTML too:

  • The <div> has a data-binding attribute that matches the name of the state variable ("currentTime")
  • This is a one-way binding that tells Reflex to automatically update the textContent of this <div> whenever the "currentTime" state variable changes!

In your JavaScript, you can use timeState.value to get and set the value of the state variable:

timeState.value = "8:54:34 AM"; // Or some other value

Instructions

  1. 🕑 For the clock:
    • Inside the setInterval callback, update timeState.value to the current time string
      • Use .toLocaleTimeString() again
    • That’s it! The UI automatically updates when you set timeState.value
  2. 🔢 For the counter:
    • Create a new State for the counter value with the name "counter" and an initial value of 0. Store it in a variable also called counter.
    • In the HTML, add data-binding="counter" to the <div> that displays the counter value
    • For the buttons, instead of using event listeners, add an onclick attribute:
      • E.g., <button onclick="counter.value++">Increment</button>
      • This lets you put JavaScript code directly in the HTML!
  3. 🎨 For the color picker:
    • Create a new State for the color value with the name "bgColor" and an initial value of "#ffffff". Store it in a variable called bgColor.
    • In the HTML, make a one-way binding to the color code display
    • For the color picker input, we’ll need a two-way binding:
      • Add data-two-way="bgColor" to the <input type="color"> element
      • When the user picks a color, the value of bgColor will be automatically updated!
    • Then, in the <body> element, you’ll need a special one-way binding:
      • First, add data-binding="bgColor" to bind it to the bgColor state variable
      • By default, Reflex will update the textContent of the <body>. We don’t want that!
        • We must specify that Reflex should set the style.backgroundColor property:
        • Add data-property="style.backgroundColor" to the <body>
  4. 📃 For the form:
    • You’ll want to create three state variables and use them:
      • name (initial value: "") for the name input
      • age (initial value: 18) for the age input
      • output (initial value: "") for the output display
    • In the HTML:
      • Add two-way bindings to the name and age inputs
      • Add a one-way binding to the output display
    • Like before, you’ll want to send a request and display the response in the output

That’s it! The code should be much cleaner; no need to get DOM elements or add event listeners.

Test your code by opening reflex.html in a preview and trying out the different features!

How does Reflex work?

We’ve given you the source code for Reflex in reflex.js. Take a moment to read through it and see if you can understand how it works.

Reflex uses the observer pattern to implement data binding. When you create a new State, it goes through the DOM and finds all elements that have a data-binding or data-two-way attribute that matches its name.

It has a getter and setter for the value property. Whenever you set a new value, it automatically updates all the bound elements in the DOM to reflect the new value.

Take a Look: Real UI Frameworks

Reflex is a simple implementation to help demonstrate the core concepts. It’s fully usable and you can use it in your own projects, but some parts are “clunky” in comparison to real frameworks (e.g., how you have a lot of data-binding and data-property attributes).

Modern UI frameworks provide data binding and much more, focusing on component-driven development to make building UIs easier.

React

For example, React lets you write HTML directly in your JavaScript using a syntax called JSX.

Then, you just reference your state variables directly in the HTML (like string interpolation), and React automatically updates the UI whenever those variables change:

function Counter() {
  const [counter, setCounter] = useState(0);

  return (
    <div>
      <p>Counter: {counter}</p>
      <button onClick={() => setCounter(counter + 1)}>Increment</button>
      <button onClick={() => setCounter(counter - 1)}>Decrement</button>
    </div>
  );
}

Here, useState creates and returns two things:

  • counter represents the state of the counter (read-only).
  • setCounter is a function that sets the value of counter and automatically updates the UI.

The Counter: {counter} magically updates the content of the <p> element whenever counter changes, without us having to manually manipulate the DOM or specify a data binding!

Think: Advantages to this approach?
  • Instead of having separate HTML and JavaScript, having them together in the same file makes it easier to see how the UI and logic are connected
  • There are no unnecessary id, data-binding, or event listeners cluttering up the code
  • Components like Counter are reusable and composable, making it easier to build complex UIs from simple building blocks
  • You don’t need to use document.createElement or .appendChild (or <template> tags) to create new DOM elements!

Svelte

Another example is Svelte, which takes a different but arguably more intuitive approach.

Similarly, Svelte lets you write HTML and JavaScript (and CSS) together, but instead of using a special syntax like JSX, you can just write almost-vanilla HTML and JavaScript:

<script>
  let counter = $state(0);
</script>

<p>Counter: {counter}</p>
<button onclick={() => counter += 1}>Increment</button>
<button onclick={() => counter -= 1}>Decrement</button>

Here, $state creates a state variable that can be read and assigned to directly. Whenever you change the value of counter, Svelte automatically updates the UI to reflect the new value!

Think: How does this compare to React and Reflex?
  • Svelte’s syntax is closer to regular HTML and JavaScript, making it familiar
  • You can assign to counter directly without needing a setter function like setCounter
  • The code looks cleaner without the extra attributes for data binding and event listeners

Conclusion

Now you know a little about how modern UI frameworks make building interactive apps easier! We encourage you to do some exploration on your own and try a tutorial for a modern UI framework like React, Vue, or Svelte.

There are a lot of them out there, but they share similar core concepts like components, state, and data binding.

Submission

For credit, submit interactivity.html and reflex.html to the corresponding assignment.