Originally posted at michaelzanggl.com. Subscribe to my newsletter to never miss out on new content.
Today we will program a very simple app and compare the implementation between VueJs and Vanilla JavaScript. For Vue we will be using Single File Components which basically means that each component lives in its own .vue
file.
If you prefer an interactive tutorial that goes through all the basic steps, check out learning by vueing.
The app we want to build has a button that counts up when you click on it.
Let's look at the Vanilla JavaScript solution.
<button id="counter">0</button>
const counterBtn = document.getElementById('counter')
counterBtn.addEventListener('click', function incrementCounter() {
const count = Number(counterBtn.innerText) + 1
counterBtn.innerText = count
})
Okay, so far so good. We could have also saved the current count inside a variable/state, increment it and update the DOM. Let's see how we can implement this.
<button id="counter"></button>
const counterBtn = document.getElementById('counter')
let count = 0
function renderCount() {
counterBtn.innerText = count
}
counterBtn.addEventListener('click', function incrementCounter() {
count = count + 1
renderCount()
})
// on init
renderCount()
One problem with this method is that we have to call the method renderCount
during the initialization to make 100% sure the count stays in sync with the DOM.
As you can see, from the very start there are multiple ways to design your application.
The first is a simple, but slightly dirty and not easily extendable way.
The second is a somewhat cleaner way that comes with some overhead.
It's important to remember though that the DOM should not be used as a datastore. So let's stick with the seond version for now.
Let's see the equivalent in a Vue Single File Component.
Since we use single file components you need to use something like Vue Cli, Laravel Mix, etc. to transpile the vue files to normal Javascript. Alternatively you can try it out in an online editor.
Let's assume we have the following wrapper component App.vue
<template>
<div>
<app-counter />
</div>
</template>
<script>
import AppCounter from './Counter'
export default {
components: { AppCounter }
}
</script>
And here is our component counter.vue
where we will spend most of our time.
<template>
<div>
<button @click="counter++">{{ counter }} </button>
</div>
</template>
<script>
export default {
data() {
return {
counter: 0,
}
},
}
</script>
In Vue you will never find something like counterBtn.innerText = count
. The UI is synced with its state/data. Let me repeat this
The UI is synced with its state/data
It may not matter so much for our simple counter, but imagine having a table where you can add, edit and delete records. Imagine you update the array in JavaScript and then somehow have to find a way to update the HTML table. Will you just reload the whole table? Find the element in HTML and then edit / remove it? Certainly, it will be messy. Vue takes care of dealing with the whole UI part for us.
But so far, installing Vue for just this is a little overkill. Let's see how our app grows once we add more features.
We want our app to show the text Good Job!
when the counter is at least 10.
This would be the Vanilla approach.
<button id="counter"></button>
<div id="inspirational-message" class="hidden">Good Job!</div>
const counterBtn = document.getElementById('counter')
const inspirationalMessageEl = document.getElementById('inspirational-message')
let count = 0
function renderCount() {
counterBtn.innerText = count
if (count >= 10) {
inspirationalMessageEl.classList.remove('hidden')
}
}
counterBtn.addEventListener('click', function incrementCounter() {
count = count + 1
renderCount()
})
// on init
renderCount()
Let's add this CSS class:
.hidden {
display: none;
}
Alright, so we had to add a new element that is now always in the DOM, a CSS class, and an if condition.
Let's check out how our codebase grows in a Vue component.
<template>
<div>
<button @click="counter++">{{ counter }} </button>
<div v-if="counter >= 10">Good Job!</div>
</div>
</template>
<script>
export default {
data() {
return {
counter: 0,
}
},
}
</script>
Wow, that was super easy! We did everything in one line of code. And if we check the DOM, there is not even a hidden div if the counter is less than 10. This is because Vue uses a virtual DOM and can therefore ship only the necessary HTML to the actual DOM.
But now the project manager of our multi million dollar app comes to us and says they also want a decrement button. Let's see who will suffer more implementing this?
In order to make a decrement button we have to remove the current count from the increment button label and add it between the increment and decrement button.
Let's see the JavaScript implementation
<button id="increment-counter">+</button>
<span id="counter"></span>
<button id="decrement-counter">-</button>
<div id="inspirational-message" class="hidden">Good Job!</div>
const counterEl = document.getElementById('counter')
const incrementCounterEl = document.getElementById('increment-counter')
const decrementCounterEl = document.getElementById('decrement-counter')
const inspirationalMessageEl = document.getElementById('inspirational-message')
let count = 0
function renderCount() {
counterEl.innerText = count
const forceToggle = count < 10
inspirationalMessageEl.classList.toggle('hidden', forceToggle)
}
incrementCounterEl.addEventListener('click', function incrementCounter() {
count = count + 1
renderCount()
})
decrementCounterEl.addEventListener('click', function decrementCounter() {
count = count - 1
renderCount()
})
// on init
renderCount()
.hidden {
display: none;
}
Well, that took us a lot of changes for a simple decrement button...
Here is the whole thing in Vue
<template>
<div>
<button @click="counter--">-</button>
{{ counter }}
<button @click="counter++">+</button>
<div v-if="counter >= 10">Good Job!</div>
</div>
</template>
<script>
export default {
data() {
return {
counter: 0,
}
},
}
</script>
Two lines! That's only two lines of code!
Okay, I gotta be fair, that Vanilla JavaScript there goes out of control. So let's refactor it first before continuing, I am not trying to trash on it after all.
class Counter {
constructor() {
this.count = 0
this.cacheDOM()
this.bindEvents()
this.render()
}
cacheDOM() {
this.counterEl = document.getElementById('counter')
this.incrementCounterEl = document.getElementById('increment-counter')
this.decrementCounterEl = document.getElementById('decrement-counter')
this.inspirationalMessageEl = document.getElementById('inspirational-message')
}
bindEvents() {
this.incrementCounterEl.addEventListener('click', () => this.countUp(1))
this.decrementCounterEl.addEventListener('click', () => this.countUp(-1))
}
render() {
this.counterEl.innerText = this.count
const forceToggle = this.count < 10
this.inspirationalMessageEl.classList.toggle('hidden', forceToggle)
}
countUp(value) {
this.count += value
this.render()
}
}
new Counter()
That's a lot better!
Now the project manager comes to us again. This time, he requests to have different inspirational messages depending on the value of the count.
Here are the specs:
< 10 -> Go on with it
10-15 -> 頑張って
16 - 25 -> Sauba!
25 - 50 -> Good Job!
You can not go below zero or above 50.
At this point there are so many ways to implement this in Vanilla JavaScript, it is hard to choose from one... How about this?
<button id="increment-counter">+</button>
<span id="counter"></span>
<button id="decrement-counter">-</button>
<div id="inspirational-message"></div>
class Counter {
constructor() {
this.count = 0
this.messages = [
{ start: 0, end: 9, message: 'Go on with it!' },
{ start: 10, end: 15, message: '頑張って!' },
{ start: 16, end: 25, message: 'Sauba' },
{ start: 26, end: 50, message: 'Good Job' },
]
this.cacheDOM()
this.bindEvents()
this.render()
}
cacheDOM() {
this.counterEl = document.getElementById('counter')
this.incrementCounterEl = document.getElementById('increment-counter')
this.decrementCounterEl = document.getElementById('decrement-counter')
this.inspirationalMessageEl = document.getElementById('inspirational-message')
}
bindEvents() {
this.incrementCounterEl.addEventListener('click', () => this.countUp(1))
this.decrementCounterEl.addEventListener('click', () => this.countUp(-1))
}
render() {
this.counterEl.innerText = this.count
const { message } = this.messages.find(({start, end}) => this.count >= start && this.count <= end)
this.inspirationalMessageEl.innerText = message
}
countUp(value) {
const newCount = this.count + value
if (newCount < 0 || newCount > 50) return
this.count = newCount
this.render()
}
}
new Counter()
This should do it. Our refactored JavaScript is now more easily extendable. We had to change the constructor
, render
method and count
method. Let's look at the Vue implementation.
<template>
<div>
<button @click="counter > 0 && counter--">-</button>
{{ counter }}
<button @click="counter < 50 && counter++">+</button>
<div>{{ message }}</div>
</div>
</template>
<script>
export default {
data() {
return {
counter: 0,
messages: [
{ start: 0, end: 9, message: 'Go on with it!' },
{ start: 10, end: 15, message: '頑張って!' },
{ start: 16, end: 25, message: 'Sauba' },
{ start: 26, end: 50, message: 'Good Job' },
],
}
},
computed: {
message() {
return this.messages
.find(({start, end}) => this.counter >= start && this.counter <= end)
.message
}
}
}
</script>
In the Vanilla JavaScript implementation we had to extend our render method. Vue has a much more elegant solution with its computed fields.
A computed field takes existing data, runs the synchronous method, in our case message()
, caches it and makes it available just as if it was actual data
.
We can also extract our decrementing and incrementing into a method.
<template>
<div>
<button @click="decrement">-</button>
{{ counter }}
<button @click="increment">+</button>
<div>{{ message }}</div>
</div>
</template>
<script>
export default {
data() {
return {
counter: 0,
messages: [
{ start: 0, end: 9, message: 'Go on with it!' },
{ start: 10, end: 15, message: '頑張って!' },
{ start: 16, end: 25, message: 'Sauba' },
{ start: 26, end: 50, message: 'Good Job' },
],
}
},
computed: {
message() {
return this.messages
.find(({start, end}) => this.counter >= start && this.counter <= end)
.message
}
},
methods: {
decrement() {
if (this.counter > 0) this.counter--
},
increment() {
if (this.counter < 50) this.counter++
},
}
}
</script>
Looking at the two implementations, both are understandable at this point. That's good! There are a couple problems we encountered with the Vanilla JavaScript implementation though. Right from the start, we had to make decisions about the best way to implement the counter. After some spec changes we also very early had to refactor it into a modular structure to keep the code readable. In general, it was harder to make the required changes.
The nice thing about Vue is that everything has its place.
Now we are about to release our counter, suddenly the pm knocks on our door and tells us that there can be multiple counters on one page. Pretty simple thing right, just copy some HTML. But wait... we used ID's all the time. That means we can only have one counter on the page... Luckily though, we modularized our code, so we only have to make some small changes to it. Let's look at the implementation.
<div class="counter-wrapper" id="counter1">
<button class="increment-counter">+</button>
<span class="counter"></span>
<button class="decrement-counter">-</button>
<div class="inspirational-message"></div>
</div>
<div class="counter-wrapper" id="counter2">
<button class="increment-counter">+</button>
<span class="counter"></span>
<button class="decrement-counter">-</button>
<div class="inspirational-message"></div>
</div>
We had to get rid of all IDs and replace them with classes.
class Counter {
constructor(wrapperEl) {
this.count = 0
this.messages = [
{ start: 0, end: 9, message: 'Go on with it!' },
{ start: 10, end: 15, message: '頑張って!' },
{ start: 16, end: 25, message: 'Sauba' },
{ start: 26, end: 50, message: 'Good Job' },
]
this.cacheDOM(wrapperEl)
this.bindEvents()
this.render()
}
cacheDOM(wrapperEl) {
this.wrapperEl = wrapperEl
this.counterEl = this.wrapperEl.querySelector('.counter')
this.incrementCounterEl = this.wrapperEl.querySelector('.increment-counter')
this.decrementCounterEl = this.wrapperEl.querySelector('.decrement-counter')
this.inspirationalMessageEl = this.wrapperEl.querySelector('.inspirational-message')
}
bindEvents() {
this.incrementCounterEl.addEventListener('click', () => this.countUp(1))
this.decrementCounterEl.addEventListener('click', () => this.countUp(-1))
}
render() {
this.counterEl.innerText = this.count
const { message } = this.messages.find(({start, end}) => this.count >= start && this.count <= end)
this.inspirationalMessageEl.innerText = message
}
countUp(value) {
const newCount = this.count + value
if (newCount < 0 || newCount > 50) return
this.count = newCount
this.render()
}
}
new Counter(document.getElementById('counter1'))
new Counter(document.getElementById('counter2'))
Let's look at the Vue implementation. Actually all we have to change is our App.vue
<template>
<div>
<app-counter />
<app-counter />
</div>
</template>
<script>
import AppCounter from './Counter'
export default {
components: { AppCounter }
}
</script>
Yup, that's it! We just had to copy paste <app-counter />
. The state inside a vue component is only accessible within that component.
Conclusion
What I wanted to demonstrate in this article is how readable and easily extendable Vue is. Compare each step between the Vanilla JavaScript and the Vue solution. In all cases the Vue solution required much less changes.
Vue, while opinionated, forces you into a clear structure.
Please also take a minute to compare the end result. Which one is more readable and therefore more easily maintainable in your opinion?
At the end you could see how easy it was to add another counter component to our app. And this is really where Vue shines, with its amazing component design. Vanilla JavaScript solutions will be far behind in readability and extendibility. But that's for another episode ;) We just barely scratched the surface of Vue.
If this article helped you, I have a lot more tips on simplifying writing software here.
Top comments (7)
Michael,
Happy New Year! I started building this and then got stuck when I tried to import my App.vue! You did not mention that one would have to use the cli to have all the pieces in place so we could support transpuilation etc.
That said, it is a great example of how Vue compares to plain JavaScript!
Cheers,
Everardo
Happy New Year!
Thanks for mentioning that. I added a note now. Alternatively to using something like webpack, you could also use Codesandbox to play around with Vue online.
How about this?
Nice article. The beauty of Vue shines when you use it with Nuxt.
I agree, Nuxt is amazing!
Awesome article! Thanks for going through all the effort of making this post!
This not only gives us a comparison, but gives us a peek into the secret ingredients of Vuejs. Thumb up, Sir.