Vue.js Basics Explained — Complete Beginner's Guide
Vue.js is a progressive JavaScript framework that makes HTML reactive — your templates update automatically whenever your data changes, no manual DOM work needed.
What You’ll Learn
- Set up Vue.js via CDN and Vite
- Understand reactive data with the spreadsheet analogy
- Use template syntax and directives (v-bind, v-if, v-for, v-on, v-model)
- Create computed properties and watchers with real use cases
- Master Vue’s lifecycle hooks
- Avoid 6 common beginner mistakes
- Build a complete interactive todo list
Why Vue Basics Matters
Every modern web app needs to update its UI when data changes. Without a framework, you’d manually select DOM elements and update them — error-prone and messy. Vue automates this with reactivity: change your data, and the UI updates instantly.
Security note: Understanding Vue Basics helps build more secure applications — a core principle at DodaTech, where tools like Durga Antivirus Pro and Doda Browser rely on solid implementation practices.
At DodaTech, Vue powers parts of the Doda Browser settings panel and the DodaZIP file manager interface, where real-time file status updates and reactive UI states are essential. Vue’s gentle learning curve makes it ideal for adding interactivity to existing pages without a full rewrite.
Your Learning Path
flowchart LR
A[JavaScript Basics] --> B[Vue Basics]
B --> C[Vue Components]
C --> D[Vue Router]
D --> E[Vue Advanced]
B:::current
classDef current fill:#42b883,color:#fff,stroke:#3aa876,stroke-width:2px
Understanding Vue’s Core Idea — The Reactive Spreadsheet
Before writing code, let’s understand what makes Vue special.
Imagine a spreadsheet:
| Cell | Formula | Value |
|---|---|---|
| A1 | 5 | |
| A2 | 10 | |
| A3 | =A1+A2 | 15 |
If you change A1 to 7, A3 automatically becomes 17. You didn’t recalculate — the spreadsheet did it.
Vue works the same way. You declare what depends on what, and Vue handles the rest.
ref()andreactive()create reactive cells (like A1, A2)computed()creates formula cells (like =A1+A2)watch()is like setting a surveillance camera — it alerts you when a value changes- The template renders automatically when any dependency changes
Keep this spreadsheet analogy in mind. It’s the single most important concept in Vue.
Setting Up Vue
Method 1: CDN (Quick Start, No Install)
<!-- The simplest way to try Vue: add a script tag.
vue.global.js loads the full Vue library from a CDN.
Perfect for prototyping or adding Vue to an existing page. -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>Method 2: Vite (Recommended for Real Projects)
# Vite creates a new Vue project with hot-reload and build tools.
# The --template vue flag scaffolds a Vue 3 project.
npm create vite@latest my-vue-app -- --template vue
cd my-vue-app
npm install # Downloads all dependencies
npm run dev # Starts a dev server with hot-reloadYour First Vue App
Let’s break down every line of this first example:
<!DOCTYPE html>
<html>
<head>
<title>My First Vue App</title>
<!-- Step 1: Load Vue from CDN.
This makes the Vue object available globally. -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<!-- Step 2: The id="app" div is Vue's "territory."
Vue will take over everything inside this element.
Think of it as a stage where Vue performs. -->
<div id="app">
<!-- Step 3: {{ message }} is Vue's template syntax.
It's like a placeholder — Vue replaces this text
with the value of message. When message changes,
this updates automatically. -->
<h1>{{ message }}</h1>
</div>
<script>
// Step 4: Get the createApp function from Vue.
// createApp is the entry point for every Vue application.
const { createApp } = Vue;
// Step 5: Create a Vue app instance.
// The object passed to createApp is called "options" —
// it configures how the app behaves.
createApp({
// data() returns the app's reactive state.
// "Reactive" means: when this value changes, the DOM updates.
// data() must be a function (not a plain object) so each
// component instance gets its own copy of the data.
data() {
return {
message: "Hello Vue!" // Any property returned here is reactive
};
}
// .mount("#app") tells Vue: "take over the #app element
// and render the template inside it."
}).mount("#app");
</script>
</body>
</html>What happens when you open this: The page displays “Hello Vue!” in an <h1>. Open your browser console and type document.querySelector('#app').__vue_app__ — you’ll see the Vue instance. Try changing message via the console, and the DOM updates instantly.
Template Syntax — HTML with Superpowers
Text Interpolation
The {{ }} syntax is called text interpolation. It’s like a calculator display — whatever expression you put inside gets evaluated and converted to text:
<template>
<p>{{ greeting }}</p> <!-- plain text variable -->
<p>{{ number + 1 }}</p> <!-- math expression -->
<p>{{ ok ? "YES" : "NO" }}</p> <!-- ternary operator -->
<p>{{ message.split("").reverse().join("") }}</p> <!-- string manipulation -->
</template>
<script>
export default {
data() {
return {
greeting: "Hello",
number: 5,
ok: true,
message: "Vue"
};
}
};
</script>Why expressions, not statements? {{ }} evaluates to a value. You can’t use if or for inside — those belong in <script>. Think of {{ }} as a calculator display, not a code editor.
Raw HTML with v-html
<!-- v-html renders raw HTML. This is POWERFUL but DANGEROUS.
If the content comes from users, attackers can inject
<script> tags (XSS attack). Only use on trusted content. -->
<p v-html="rawHtml"></p>v-html with user-generated content. Attackers can inject malicious scripts. Only use it with trusted HTML from your own server.Directives — HTML Superpowers
Directives are special HTML attributes starting with v-. They give HTML abilities it doesn’t normally have — like conditional rendering or looping. Think of them as superpowers for HTML.
v-bind — Connecting Data to Attributes
Normally, HTML attributes are static strings. With v-bind, you can make them dynamic — they react to your data:
<!-- v-bind binds an attribute to a JavaScript expression.
Here, imgUrl comes from data(), and the src attribute
updates whenever imgUrl changes. -->
<img v-bind:src="imageUrl" />
<!-- Shorthand: : instead of v-bind: -->
<a :href="link">Dynamic Link</a>
<!-- Dynamic classes: { active: isActive } means
"add the 'active' class when isActive is true" -->
<div :class="{ active: isActive }">Styled</div>
<!-- Dynamic styles -->
<div :style="{ color: textColor, fontSize: size + 'px' }">Inline</div>Conditional Rendering — v-if / v-show
<!-- v-if removes the element from the DOM entirely
when the condition is false. It's like cutting paper with scissors. -->
<p v-if="score >= 90">Excellent</p>
<p v-else-if="score >= 70">Good</p>
<p v-else>Keep Trying</p>
<!-- v-show just hides with CSS display:none.
The element is still in the DOM, just invisible.
It's like putting something in a drawer vs. throwing it away. -->
<p v-show="isVisible">Always rendered, hidden with display:none</p>v-if vs v-show comparison:
| Scenario | Use | Why |
|---|---|---|
| Content rarely changes | v-if | Saves DOM nodes, less memory |
| Content toggles frequently | v-show | Faster toggle, no DOM teardown/rebuild |
| Initial render not needed | v-if | Avoids rendering hidden content |
| Visibility changes in ms | v-show | CSS toggle is instant |
v-for — List Rendering
<ul>
<!-- :key is REQUIRED — helps Vue track items by identity, not position.
Without :key, if you reorder the list, Vue might think
"the first item's text changed" instead of "items moved positions."
Always use a unique, stable ID. -->
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
<!-- With index -->
<li v-for="(item, index) in items" :key="item.id">
{{ index }}: {{ item.name }}
</li>
<!-- Iterating objects -->
<li v-for="(value, key, index) in obj" :key="key">
{{ index }}. {{ key }}: {{ value }}
</li>v-on — Event Handling
<button v-on:click="count++">+1</button> <!-- Inline expression -->
<button @click="handleSubmit">Submit</button> <!-- Shorthand: @ -->
<button @click="say('Hello', $event)">Say Hello</button> <!-- With argument -->
<!-- Event modifiers — chain after the event name with a dot:
.prevent = preventDefault(), .stop = stopPropagation() -->
<form @submit.prevent="onSubmit">...</form> <!-- No page reload -->
<a @click.stop="doThis">Click</a> <!-- No bubbling -->
<input @keyup.enter="submit" /> <!-- Enter key only -->v-model — Two-Way Binding
v-model creates a two-way connection: when the input changes, data updates. When data changes, the input updates. It’s like a two-way mirror between your data and the form:
<input v-model="username" placeholder="Username" />
<textarea v-model="bio"></textarea>
<input type="checkbox" v-model="agree" />
<input type="radio" v-model="picked" value="option-a" />
<select v-model="selected">
<option disabled value="">Select</option>
<option>A</option>
<option>B</option>
</select>
<!-- Modifiers change how v-model behaves -->
<input v-model.lazy="name" /> <!-- Sync on change (blur), not keystroke -->
<input v-model.number="age" /> <!-- Convert string to number automatically -->
<input v-model.trim="email" /> <!-- Remove leading/trailing whitespace -->v-model vs manual binding:
| Approach | Code | Best For |
|---|---|---|
| v-model | <input v-model="name"> | Quick forms, simple data |
| Manual | <input :value="name" @input="name = $event.target.value"> | Custom logic on update, transformations |
Computed Properties — The Formula Cells
Remember the spreadsheet analogy? Computed properties are Vue’s formula cells — they derive values from existing data and cache the result. A computed property only recalculates when its dependencies change, not every time the template re-renders:
<script>
export default {
data() {
return {
todos: [
{ text: "Learn Vue", done: false },
{ text: "Build a project", done: true }
]
};
},
computed: {
// activeTodos is like =FILTER(todos, done=false).
// It recalculates ONLY when todos changes.
// The result is CACHED — huge performance win for expensive operations.
activeTodos() {
return this.todos.filter(t => !t.done);
},
todoCount() {
return this.todos.length;
}
}
};
</script>
<template>
<p>{{ activeTodos.length }} of {{ todoCount }} remaining</p>
</template>Computed vs method: A method runs every time it’s called. A computed property caches its result until dependencies change. For filtering 10,000 items, computed is far more efficient — the method would re-filter on every render.
Watchers — Surveillance Cameras
Watchers let you react to data changes for side effects — things that don’t produce a value but need to happen when data changes, like API calls or saving to localStorage:
<script>
export default {
data() {
return {
searchQuery: "",
results: []
};
},
watch: {
// This "camera" watches searchQuery.
// newVal is the new value after the change.
// oldVal is the previous value before the change.
searchQuery(newVal, oldVal) {
// Only trigger API call if query is long enough
// to avoid too many requests (debouncing).
if (newVal.length > 2) {
this.fetchResults(newVal);
}
}
},
methods: {
fetchResults(query) {
fetch(`/api/search?q=${query}`)
.then(res => res.json())
.then(data => this.results = data);
}
}
};
</script>Watcher vs computed — when to use which:
| Need | Use | Example |
|---|---|---|
| Derive a value | computed | Full name from first + last |
| React with side effect | watch | Save to localStorage on change |
| Debounced API call | watch | Search autocomplete, form auto-save |
Class & Style Bindings
<template>
<!-- Object syntax: add/remove classes based on boolean flags -->
<div :class="{ active: isActive, 'text-danger': hasError }">Content</div>
<!-- Array syntax: combine multiple class variables -->
<div :class="[baseClass, errorClass]">Content</div>
<!-- Conditional class inside an array -->
<div :class="[baseClass, isActive ? 'active' : '']">Content</div>
<!-- Style binding: camelCase or quoted strings -->
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }">Content</div>
<!-- Merge multiple style objects -->
<div :style="[baseStyles, overrides]">Content</div>
</template>Lifecycle Hooks — Vue’s Timeline
Every Vue component goes through stages: create → mount → update → destroy. You can run code at each stage using lifecycle hooks:
flowchart LR
A[beforeCreate] --> B[created]
B --> C[beforeMount]
C --> D[mounted]
D --> E[beforeUpdate]
E --> F[updated]
D --> G[beforeUnmount]
G --> H[unmounted]
style D fill:#42b883,color:#fff
<script>
export default {
beforeCreate() { console.log("instance initialized — nothing reactive yet"); },
created() { console.log("reactive data ready"); },
beforeMount() { console.log("about to render to DOM"); },
mounted() { console.log("DOM is in the page — safe to fetch data"); },
beforeUpdate() { console.log("data changed, about to re-render"); },
updated() { console.log("DOM re-rendered"); },
beforeUnmount() { console.log("cleanup time"); },
unmounted() { console.log("gone — remove timers and listeners"); }
};
</script>Where to put common logic:
- Fetch data:
mounted()— the component is in the DOM - Set up timers:
mounted(), clean up inunmounted() - Log page views:
mounted()or route guard - Auto-save drafts:
watch+ debounce
Options API vs Composition API
| Feature | Options API | Composition API |
|---|---|---|
| Structure | data, methods, computed sections | ref(), functions, composables |
| Code organization | Split by option type | Split by logical concern |
| Reuse logic | Mixins (name conflicts possible) | Composables (no conflicts) |
| TypeScript support | Limited | Excellent |
| When to use | Simple components, legacy code | New code, complex apps |
This tutorial uses Options API — it’s more explicit for beginners. The advanced tutorial covers Composition API.
Common Mistakes
1. data must be a function, not an object
// ❌ Bad — all component instances share the same state
data: { count: 0 }
// ✅ Good — each instance gets its own copy
data() { return { count: 0 }; }Why? If data were a plain object, clicking a button in one instance would change the count in ALL instances. The function creates a fresh copy per instance.
2. Missing :key in v-for
Without a unique :key, Vue may reuse elements incorrectly and cause state bugs when the list changes. Always use a stable, unique identifier.
3. Using v-if and v-for together on the same element
<!-- ❌ Bad: v-for runs first (higher priority), so v-if runs on every item.
Vue shows a warning about this in development. -->
<li v-for="item in items" v-if="item.visible" :key="item.id">
<!-- ✅ Good: filter with a computed property instead -->
<li v-for="item in visibleItems" :key="item.id">4. Mutating props directly
Props flow down from parent to child. Never modify a prop in the child — emit an event to notify the parent instead.
5. Overusing watchers
If you’re deriving a value (like fullName from firstName + lastName), use computed, not watch. Watchers are for side effects, not value derivation.
6. Forgetting .value in Composition API
In <script setup>, refs require .value inside <script> but NOT in templates. This trips up everyone at first.
Practice Questions
1. What does v-model do?
Creates two-way data binding between a form input and a data property. Changes in either direction sync automatically.
2. What’s the difference between v-if and v-show?
v-if adds/removes the DOM element. v-show toggles CSS display: none. Use v-if for rare changes, v-show for frequent toggles.
3. When should you use a computed property instead of a method?
When the result depends on reactive data and you want caching. Computed properties only recalculate when dependencies change.
4. Which lifecycle hook is best for initial data fetching?
mounted() — the component is in the DOM, and you can safely update reactive state with API results.
5. Why must data be a function in a component?
So each instance gets its own copy. If data were a plain object, all instances would share the same state.
Challenge: Create a Vue app with a text input and a live character counter. Show “X characters remaining” (max 100). Turn the counter red when below 10. Disable input when limit is reached.
FAQ
Try It Yourself
Copy this into a new HTML file and open it in your browser:
<!DOCTYPE html>
<html>
<head>
<title>Vue Basics Sandbox</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<style>
body { font-family: sans-serif; max-width: 500px; margin: 40px auto; padding: 0 20px; }
.counter { display: flex; gap: 12px; align-items: center; margin: 20px 0; }
.counter button { padding: 8px 16px; font-size: 18px; cursor: pointer; background: #42b883; color: white; border: none; border-radius: 4px; }
.counter span { font-size: 24px; font-weight: bold; min-width: 40px; text-align: center; }
.greeting { padding: 16px; background: #f0fdf4; border-radius: 8px; border: 1px solid #42b883; }
.remaining { margin-top: 8px; font-size: 14px; }
.remaining.warning { color: #e74c3c; font-weight: bold; }
</style>
</head>
<body>
<div id="app">
<h1>Vue Counter</h1>
<div class="counter">
<button @click="count--">-</button>
<span>{{ count }}</span>
<button @click="count++">+</button>
</div>
<p>Double: {{ double }}</p>
<hr>
<h2>Live Greeting</h2>
<input v-model="name" placeholder="Enter your name">
<div class="greeting">
Hello, <strong>{{ name || "Stranger" }}</strong>!
</div>
<hr>
<h2>Character Limit</h2>
<input v-model="text" maxlength="100" placeholder="Type something...">
<p :class="{ remaining: true, warning: remaining < 10 }">
{{ remaining }} characters remaining
</p>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
count: 0,
name: "",
text: ""
};
},
computed: {
double() { return this.count * 2; },
remaining() { return 100 - this.text.length; }
}
}).mount("#app");
</script>
</body>
</html>What to try: Click + and - to see reactivity in action. Type in the input — the greeting updates live. Watch the character counter decrease and turn red below 10.
What’s Next
| Resource | Description |
|---|---|
| https://tutorials.dodatech.com/frameworks/vue/vue-components/ | Learn components, props, slots, and emits |
| https://tutorials.dodatech.com/frameworks/vue/vue-router/ | Client-side routing with Vue Router |
| JavaScript | Solidify your JavaScript fundamentals |
| TypeScript | Add type safety to your Vue apps |
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
What’s Next
Congratulations on completing this Vue Basics tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro