DEV Community

Cover image for Vue vs Vanilla JavaScript - Beginner's Guide
Michael Z
Michael Z

Posted on • Updated on • Originally published at michaelzanggl.com

Vue vs Vanilla JavaScript - Beginner's Guide

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>
Enter fullscreen mode Exit fullscreen mode
const counterBtn = document.getElementById('counter')

counterBtn.addEventListener('click', function incrementCounter() {
    const count = Number(counterBtn.innerText) + 1
    counterBtn.innerText = count
})
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
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()

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
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()
Enter fullscreen mode Exit fullscreen mode

Let's add this CSS class:

.hidden {
    display: none;
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
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()
Enter fullscreen mode Exit fullscreen mode
.hidden {
    display: none;
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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! 
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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'))
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
umbundu profile image
Everardo T. Cunha

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

Collapse
 
michi profile image
Michael Z

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.

Collapse
 
johngalt profile image
John Galt • Edited

How about this?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Counter</title>
</head>
<body>
    <template id="counter-template">
        <div class="counter-component">
            <button class="increment-counter">+</button>
            <span class="counter">0</span>
            <button class="decrement-counter">-</button>
            <div class="inspirational-message"></div>
        </div>
    </template>
    <div id="root"></div>

    <script>
        class Counter {
            data = {
                _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' },
                ]
            };

            rootEl;

            constructor() {
                let _this = this;

                var template = document.getElementById("counter-template");
                this.rootEl = template.content.cloneNode(true).children[0];
                document.querySelector("#root").appendChild(this.rootEl);


                // define properties
                Object.defineProperty(this.data, "counter", {
                    get() { return this._counter; },
                    set(value) {
                        this._counter = value;
                        _this.updateUI();
                    }
                });
                let incBtn = this.rootEl.querySelector(".increment-counter");
                incBtn.addEventListener('click', (e) => _this.increment(e));

                let decBtn = this.rootEl.querySelector(".decrement-counter");
                decBtn.addEventListener('click', (e) => _this.decrement(e));
                this.updateUI()
            }
            updateUI() {
                const count = this.data.counter;
                const { message } = this.data.messages.find(({start, end}) => count >= start && count <= end)
                this.rootEl.querySelector(".counter").innerHTML = count;
                this.rootEl.querySelector(".inspirational-message").innerText = message
            }
            increment(e) {
                if (this.data.counter < 50) {
                    this.data.counter++;
                }
            }
            decrement(e) {
                if (this.data.counter > 0) {
                    this.data.counter--
                }
            }
        }
        var pageComponents = [
            new Counter(),
            new Counter()
        ];
    </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
farzadso profile image
Farzad Soltani

Nice article. The beauty of Vue shines when you use it with Nuxt.

Collapse
 
michi profile image
Michael Z

I agree, Nuxt is amazing!

Collapse
 
jackedwardlyons profile image
Jack Lyons

Awesome article! Thanks for going through all the effort of making this post!

Collapse
 
liguyu profile image
liguyu

This not only gives us a comparison, but gives us a peek into the secret ingredients of Vuejs. Thumb up, Sir.