In clean front-end code, data controls the ui, not the other way around.
Your JavaScript needs to be the source of truth in whatever you're building. This rings true from the complexities of a Next.js application to a tabs section in a Webflow site.
So what do you do when your variable data changes? How can you update the ui once you've updated a variable? That's what reactivity solves. It provides convenient ways to listen for changes in data and run functions when they occur.
Prerequisites.
This article assumes the following:
- You have a basic understanding of JavaScript
- What's a variable
- What's a function
- What's an object
- Manipulating the DOM with event listeners and selectors
- You've built some kind of complex UI before (Webflow or not)
- Tabs
- FAQs
- Calculator
- Anything that requires you to track data, even just the current index.
You want to handle these kinds of builds consistently.
In this Article
I'm going to walk you through
- Why can't you write
.addEventListeneron your variables? - Why we want our data to control our ui
- What reactivity does for us
- How to create reactive variables
- How to listen for changes on those variables
- Use cases for this technique
Why can't you write .addEventListener on your variables?
Adding event listeners is something we do all the time. So what's the hoo-haa about reactivity? Why can't we write
let foo = "bar"
foo.addEventListener('change', ()=>{ console.log('foo has changed')})
foo = "new"// console: "foo has changed"Because .addEventListener is a method provided by the DOM, for the DOM. Any DOM node (fancy term for HTML element) on your page has a selection of these methods available to it. They also have unique sets of events they can emit. Like "click", "input", "focus" and more.
Plain JavaScript variables do not have these methods attached. So we have to engineer our own way.
Frameworks like React, Svelte, Vue, etc…. Have this kind of reactive system built into them. But we're Webflow developers. So what do we do?
We bring in our own. In our case, that's Vue Reactivity. An easy standalone framework that powers Vue.
Why do we want data to control our frontend?
In any given UI. We will have our state.
I'll illustrate this with an example. A simple tabs section. Our state will be the index that we're currently on.
Quick aside to clear up vocab: State = data that explains how things should look to the user. If our state says index 1, then the user should see the 1st tab active.
However, there are many ways this state can be changed.
- The user clicks a tab link.
- The tab autoplays
- The user navigates with their keyboard
Without reactivity, we'd handle the data and UI changes within each event.
// user clicks a tab link
currentIndex = 0;
let tabLinks = document.querySelectorAll('.tab_link');
tabLinks.forEach((tab, index) => {
tab.addEventListener('click', () => {
currentIndex = index;
// change your ui here
tabLinks.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
});
});
// autoplay
setInterval(() => {
currentIndex = (currentIndex + 1) % tabLinks.length;
// change your ui here
tabLinks.forEach(t => t.classList.remove('active'));
tabLinks[currentIndex].classList.add('active');
}, 3000);
// keyboard navigation
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight') {
currentIndex++;
// change your ui here
tabLinks.forEach(t => t.classList.remove('active'));
tabLinks[currentIndex].classList.add('active');
}
});See how easy it is for that to get messy? You're changing the UI and the state inside each event. Making it much more challenging to add to this module with new events and triggers. Before you know it, you're in a web of spaghetti code trying to figure out what's caused a change.
This same example using Vue would be
// state
const currentIndex = ref(0);
const tabLinks = document.querySelectorAll('.tab_link');
// watch the index — when it changes, update the UI
watch(currentIndex, (index) => {
updateUI(index);
});
// user clicks a tab
tabLinks.forEach((tab, index) => {
tab.addEventListener('click', () => {
currentIndex.value = index;
});
});
// autoplay
setInterval(() => {
currentIndex.value =
(currentIndex.value + 1) % tabLinks.length;
}, 3000);
// keyboard navigation
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight') {
currentIndex.value++;
}
});
// change your ui here
function updateUI(index) {
// change your ui here
tabLinks.forEach(t => t.classList.remove('active'));
tabLinks[index]?.classList.add('active');
}That's much simpler to understand what's happening
Whenever the index changes, we update the ui, making this much easier for us to add to. If you wanted to add more functionality, you'd just make sure that it changes the currentIndex function, and reactivity will handle the rest.
What is reactivity doing for us?
When we create a reactive variable, we're essentially wrapping that variable in an object to create a proxy. So now whenever you change your proxy variable, it hijacks the native set and get functionality and does some new and wonderful things for us.
Explaining proxy objects and what exactly is being hijacked is beyond the scope of this article. I do think it's important to know that you're essentially wrapping your data with some extra functionality.
How do we create a reactive variable?
I'm glad you asked.
Ref()
ref() is a function that creates your reactive variable. You can access the value using the .value property.
The end. …Is it more nuanced than that?
A little. But not much.
let state = ref(0) wraps our value (0) in an object that we can access using .value
let state = ref(0)
state.value // 0We can also do that with an object.
let state = ref({
count: 1,
name: 'james'
})
state.value.name // 'james'Quick aside: We can also use reactive(). But from my experience, it's basically the same with slight nuances. For the sake of this article, I think it's easier to just use ref(), but if you want to know the differencee read this
A reactive variable on its own is pretty useless though. We've just made it marginally more complicated to access our data. Now we want to listen for changes to our variable and do fun stuff.
How to listen for changes in those variables
I present…. watch()
Man I love it when names are easy to understand.
The watch() function will watch your variable and run functionality when it sees a change. It takes two arguments.
- The reactive variable (or a function that returns an array of reactive variables)
- The function you want to run when that variable changes.
let state = ref({
count: 1,
name: 'james'
});
console.log(state.value.count); // 0
watch(state, (oldValue, newValue) => {
console.log('going from ', oldValue, 'to ', newValue);
});
state.value = 1; // our watch() function will trigger nowYou can also listen to specific props inside a reactive variable. To do that, in your first argument you need to pass through a function that returns the values you want. You can pass as many as you want in an array like the example below.
const state = ref({
count: 1,
name: 'james'
});
// only track `count`
watch(
() => {
// function instead of the variable, but it returns the values we're watching
return [state.value.count];
},
([newCount], [oldCount]) => {
console.log(oldCount, '→', newCount);
}
);
state.value.count = 2; // triggers watch
state.value.name = 'bob'; // does NOT trigger watchA helpful technique if you're listening to specific values, they don't have to be from the same reactive variable.
const count = ref({
count: 1
});
const name = ref({
name: 'james'
});
// watch specific values, even from different refs
watch(
() => {
// function instead of the variable,
// but it returns the values we're watching
return [
count.value.count,
name.value.name
];
},
([newCount, newName], [oldCount, oldName]) => {
console.log('count:', oldCount, '→', newCount);
console.log('name:', oldName, '→', newName);
}
);
count.value.count = 2; // triggers watch
name.value.name = 'bob'; // also triggers watchUse cases
Not going to bore you now. You've come so far!
To get you excited about why this skill is valuable, here's a light collection of things that would benefit from using reactivity!
- Persisting state (e.g. saving to localStorage)
- Triggering API calls when inputs change
- Running form validation as fields change
- Sending analytics or tracking events
- Responding to external data (WebSockets, Firebase, Supabase)
- Tabs, accordions, FAQs, index-based UI
- Managing page transitions
- Filters & sorting (finsweet filters use Vue reactivity)
- Advancing stages in multi-step forms
- Syncing pricing pages (currency, plan, timeframe)
So you've learnt what reactivity is, why we need it, how to make a reactive variable, how to listen to changes on that variable and a handful of use cases to use this.
I've created 3 Webflow cloneables for you to check out. Each one is slightly more complex than the last, slowly incorporating features of reactivity. Check them out here.
- Simple number counter
- Matching text inputs to the ui
- Object based reactivity demo