Vue.js - The Gentle Introduction to Modern Frameworks
Remember all that DOM manipulation and event handling from the vanilla JavaScript version? Vue.js is about to make you feel like you have superpowers.
Here’s the thing about Vue.js - it’s not just about writing less code (though you definitely will). It’s about thinking differently. Instead of manually updating the DOM every time your data changes, you describe what the UI should look like based on your data, and Vue handles all the updates automatically. It’s like having a really smart assistant who knows exactly when and how to update your page.
Why Vue.js Clicked for Me (And Will for You Too)
I’ve worked with React, Angular, and Vue professionally, and Vue consistently has the gentlest learning curve. You can start using Vue by just including it from a CDN and gradually adopt more features as you need them. No complex build tools required to get started.
The Progressive Framework Approach
Vue calls itself “progressive” because you can adopt it incrementally:
- Start simple: Just add Vue to a single page or component
- Add features gradually: Routing, state management, build tools as needed
- Scale up: Full single-page applications when you’re ready
Vue’s Gentle Learning Curve
Coming from vanilla JavaScript, Vue feels natural because:
- Templates look like HTML (because they are HTML)
- You can use regular CSS (no CSS-in-JS required)
- The JavaScript is just JavaScript with some Vue-specific helpers
- Everything is optional - use what you need, ignore the rest
Vue Fundamentals Through Our Task Manager
Let’s rebuild our task manager step by step, starting with the most basic Vue setup and progressively adding features.
Setting Up Vue (No Build Tools Required)
We’ll start with Vue from a CDN to focus on the concepts:
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Vue.js Task Manager</title>
7 <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
8 <link rel="stylesheet" href="styles.css">
9</head>
10<body>
11 <div id="app">
12 <!-- Vue template goes here -->
13 </div>
14 <script src="app.js"></script>
15</body>
16</html>
Basic Vue Instance
1const { createApp } = Vue;
2
3createApp({
4 data() {
5 return {
6 message: 'Hello Vue.js!'
7 }
8 }
9}).mount('#app');
That’s it! You now have a Vue application. The data()
function returns an object with all your reactive data, and anything you put in there will automatically update the UI when it changes.
Building the Task Manager Template
Here’s where Vue starts to shine. Instead of building DOM elements in JavaScript like we did before, we describe the UI declaratively:
1<div id="app">
2 <div class="container">
3 <header>
4 <h1>Vue.js Task Manager</h1>
5 <div class="stats">
6 <span>{{ totalTasks }} tasks</span>
7 <span>{{ completedTasks }} completed</span>
8 </div>
9 </header>
10
11 <!-- Add Task Form -->
12 <form @submit.prevent="addTask" class="add-task-form">
13 <div class="form-group">
14 <input
15 v-model="newTask.title"
16 type="text"
17 placeholder="What needs to be done?"
18 required
19 >
20 <select v-model="newTask.priority">
21 <option value="low">Low Priority</option>
22 <option value="medium">Medium Priority</option>
23 <option value="high">High Priority</option>
24 </select>
25 </div>
26 <div class="form-group">
27 <textarea
28 v-model="newTask.description"
29 placeholder="Add a description (optional)"
30 rows="2"
31 ></textarea>
32 </div>
33 <button type="submit" :disabled="isLoading">
34 {{ isLoading ? 'Adding...' : 'Add Task' }}
35 </button>
36 </form>
37
38 <!-- Filter Buttons -->
39 <div class="filters">
40 <button
41 v-for="filter in filters"
42 :key="filter.key"
43 class="filter-btn"
44 :class="{ active: currentFilter === filter.key }"
45 @click="currentFilter = filter.key"
46 >
47 {{ filter.label }}
48 </button>
49 </div>
50
51 <!-- Loading State -->
52 <div v-if="isLoading" class="loading">
53 <div class="spinner"></div>
54 <p>Loading tasks...</p>
55 </div>
56
57 <!-- Error Message -->
58 <div v-if="errorMessage" class="error-message">
59 {{ errorMessage }}
60 </div>
61
62 <!-- Task List -->
63 <ul v-if="!isLoading" class="task-list">
64 <li
65 v-for="task in filteredTasks"
66 :key="task.id"
67 class="task-item"
68 :class="{ completed: task.completed }"
69 >
70 <input
71 type="checkbox"
72 :checked="task.completed"
73 @change="toggleTask(task.id, $event.target.checked)"
74 class="task-checkbox"
75 >
76 <div class="task-content">
77 <div class="task-title">{{ task.title }}</div>
78 <div v-if="task.description" class="task-description">
79 {{ task.description }}
80 </div>
81 <div class="task-meta">
82 <span class="priority-badge" :class="`priority-${task.priority}`">
83 {{ task.priority }}
84 </span>
85 <span>Created {{ formatDate(task.created_at) }}</span>
86 </div>
87 </div>
88 <div class="task-actions">
89 <button @click="deleteTask(task.id)" class="delete-btn">
90 Delete
91 </button>
92 </div>
93 </li>
94 </ul>
95
96 <!-- Empty State -->
97 <div v-if="!isLoading && filteredTasks.length === 0" class="empty-state">
98 <h3>No tasks found</h3>
99 <p>{{ emptyStateMessage }}</p>
100 </div>
101 </div>
102</div>
Look at how much cleaner this is! No getElementById
, no innerHTML
, no manual event listeners. Vue handles all of that for you.
Vue Template Syntax That Actually Makes Sense
Let’s break down the Vue-specific syntax from the template:
Interpolation (Mustache Syntax)
1<span>{{ totalTasks }} tasks</span>
This displays the value of totalTasks
from your data. It updates automatically when the value changes.
Directives (v- attributes)
v-model - Two-way data binding:
1<input v-model="newTask.title" type="text">
This automatically keeps the input value in sync with newTask.title
. When the user types, the data updates. When the data changes, the input updates.
v-for - Loop through arrays:
1<li v-for="task in filteredTasks" :key="task.id">
2 {{ task.title }}
3</li>
This creates one <li>
for each task. The :key
helps Vue track which items have changed.
v-if / v-else - Conditional rendering:
1<div v-if="isLoading">Loading...</div>
2<div v-else-if="errorMessage">{{ errorMessage }}</div>
3<div v-else>Content here</div>
@click (shorthand for v-on:click) - Event handling:
1<button @click="deleteTask(task.id)">Delete</button>
:class (shorthand for v-bind:class) - Dynamic classes:
1<li :class="{ completed: task.completed, urgent: task.priority === 'high' }">
Event Modifiers
1<form @submit.prevent="addTask">
The .prevent
modifier automatically calls preventDefault()
on the event.
The Vue Application Logic
Now let’s implement the JavaScript side. Notice how much simpler this is compared to our vanilla JavaScript version:
1const { createApp } = Vue;
2
3// API service (same as before, but cleaner)
4const api = {
5 baseURL: 'https://api.taskmanager.dev/v1',
6
7 async request(endpoint, options = {}) {
8 try {
9 const response = await fetch(`${this.baseURL}${endpoint}`, {
10 headers: { 'Content-Type': 'application/json' },
11 ...options
12 });
13
14 if (!response.ok) {
15 throw new Error(`HTTP ${response.status}: ${response.statusText}`);
16 }
17
18 return await response.json();
19 } catch (error) {
20 console.error('API Error:', error);
21 throw error;
22 }
23 },
24
25 getTasks(filters = {}) {
26 const params = new URLSearchParams(filters).toString();
27 return this.request(`/tasks${params ? `?${params}` : ''}`);
28 },
29
30 createTask(task) {
31 return this.request('/tasks', {
32 method: 'POST',
33 body: JSON.stringify(task)
34 });
35 },
36
37 updateTask(id, updates) {
38 return this.request(`/tasks/${id}`, {
39 method: 'PUT',
40 body: JSON.stringify(updates)
41 });
42 },
43
44 deleteTask(id) {
45 return this.request(`/tasks/${id}`, {
46 method: 'DELETE'
47 });
48 }
49};
50
51// Vue application
52createApp({
53 data() {
54 return {
55 tasks: [],
56 currentFilter: 'all',
57 isLoading: false,
58 errorMessage: '',
59 newTask: {
60 title: '',
61 description: '',
62 priority: 'medium'
63 },
64 filters: [
65 { key: 'all', label: 'All' },
66 { key: 'pending', label: 'Pending' },
67 { key: 'completed', label: 'Completed' }
68 ]
69 };
70 },
71
72 computed: {
73 // Computed properties automatically update when their dependencies change
74 filteredTasks() {
75 switch (this.currentFilter) {
76 case 'completed':
77 return this.tasks.filter(task => task.completed);
78 case 'pending':
79 return this.tasks.filter(task => !task.completed);
80 default:
81 return this.tasks;
82 }
83 },
84
85 totalTasks() {
86 return this.tasks.length;
87 },
88
89 completedTasks() {
90 return this.tasks.filter(task => task.completed).length;
91 },
92
93 emptyStateMessage() {
94 if (this.currentFilter === 'all') {
95 return 'Add your first task above to get started!';
96 }
97 return `No ${this.currentFilter} tasks at the moment.`;
98 }
99 },
100
101 methods: {
102 async loadTasks() {
103 try {
104 this.isLoading = true;
105 this.errorMessage = '';
106
107 const response = await api.getTasks();
108 this.tasks = response.tasks || response;
109 } catch (error) {
110 this.errorMessage = 'Failed to load tasks. Please try again.';
111 } finally {
112 this.isLoading = false;
113 }
114 },
115
116 async addTask() {
117 if (!this.newTask.title.trim()) {
118 this.errorMessage = 'Task title is required';
119 return;
120 }
121
122 try {
123 this.isLoading = true;
124 this.errorMessage = '';
125
126 const task = await api.createTask({
127 title: this.newTask.title.trim(),
128 description: this.newTask.description.trim() || null,
129 priority: this.newTask.priority,
130 completed: false
131 });
132
133 // Add to the beginning of the array
134 this.tasks.unshift(task);
135
136 // Clear the form
137 this.newTask = {
138 title: '',
139 description: '',
140 priority: 'medium'
141 };
142 } catch (error) {
143 this.errorMessage = 'Failed to create task. Please try again.';
144 } finally {
145 this.isLoading = false;
146 }
147 },
148
149 async toggleTask(taskId, completed) {
150 const task = this.tasks.find(t => t.id === taskId);
151 if (!task) return;
152
153 const originalState = task.completed;
154 task.completed = completed; // Optimistic update
155
156 try {
157 await api.updateTask(taskId, { ...task, completed });
158 } catch (error) {
159 // Revert on failure
160 task.completed = originalState;
161 this.errorMessage = 'Failed to update task. Please try again.';
162 }
163 },
164
165 async deleteTask(taskId) {
166 if (!confirm('Are you sure you want to delete this task?')) {
167 return;
168 }
169
170 const originalTasks = [...this.tasks];
171
172 // Optimistic update
173 this.tasks = this.tasks.filter(t => t.id !== taskId);
174
175 try {
176 await api.deleteTask(taskId);
177 } catch (error) {
178 // Revert on failure
179 this.tasks = originalTasks;
180 this.errorMessage = 'Failed to delete task. Please try again.';
181 }
182 },
183
184 formatDate(dateString) {
185 return new Date(dateString).toLocaleDateString('en-US', {
186 month: 'short',
187 day: 'numeric',
188 hour: '2-digit',
189 minute: '2-digit'
190 });
191 },
192
193 clearError() {
194 this.errorMessage = '';
195 }
196 },
197
198 // Lifecycle hook - runs after the component is mounted
199 async mounted() {
200 await this.loadTasks();
201 }
202}).mount('#app');
Computed Properties - Vue’s Secret Weapon
One of Vue’s most powerful features is computed properties. These are like data properties, but they’re calculated based on other data and automatically update when their dependencies change:
1computed: {
2 filteredTasks() {
3 // This automatically recalculates whenever tasks or currentFilter changes
4 switch (this.currentFilter) {
5 case 'completed':
6 return this.tasks.filter(task => task.completed);
7 case 'pending':
8 return this.tasks.filter(task => !task.completed);
9 default:
10 return this.tasks;
11 }
12 },
13
14 taskStats() {
15 const total = this.tasks.length;
16 const completed = this.tasks.filter(t => t.completed).length;
17 const pending = total - completed;
18
19 return { total, completed, pending };
20 }
21}
In our vanilla JavaScript version, we had to manually recalculate and re-render everything whenever the data changed. Vue does this automatically.
Vue vs Vanilla JavaScript - The Comparison
Let’s look at the same functionality side by side:
Adding a New Task
Vanilla JavaScript:
1async handleAddTask() {
2 // Get form values
3 const title = document.getElementById('task-title').value.trim();
4 const description = document.getElementById('task-description').value.trim();
5
6 // Validate
7 if (!title) {
8 showError('Task title is required');
9 return;
10 }
11
12 // Update button state
13 const addButton = document.getElementById('add-button');
14 addButton.disabled = true;
15 addButton.textContent = 'Adding...';
16
17 try {
18 // Create task
19 const newTask = await API.createTask({title, description, priority});
20
21 // Update local state
22 this.tasks.unshift(newTask);
23
24 // Clear form
25 document.getElementById('add-task-form').reset();
26
27 // Re-render everything
28 this.renderTasks();
29 this.updateStats();
30 } catch (error) {
31 showError('Failed to create task');
32 } finally {
33 // Restore button state
34 addButton.disabled = false;
35 addButton.textContent = 'Add Task';
36 }
37}
Vue.js:
1async addTask() {
2 if (!this.newTask.title.trim()) {
3 this.errorMessage = 'Task title is required';
4 return;
5 }
6
7 try {
8 this.isLoading = true;
9 const task = await api.createTask(this.newTask);
10 this.tasks.unshift(task);
11
12 // Clear form
13 this.newTask = { title: '', description: '', priority: 'medium' };
14 } catch (error) {
15 this.errorMessage = 'Failed to create task';
16 } finally {
17 this.isLoading = false;
18 }
19}
The Vue version is shorter, clearer, and Vue automatically handles:
- Form clearing (because
newTask
is bound withv-model
) - Button state updates (because
:disabled="isLoading"
) - UI re-rendering (because the template is reactive)
- Statistics updates (because computed properties auto-update)
Component Organization - Breaking It Down
As your app grows, you’ll want to break it into components. Here’s how you might organize the task manager:
1// TaskItem component
2const TaskItem = {
3 props: ['task'],
4 emits: ['toggle', 'delete'],
5 template: `
6 <li class="task-item" :class="{ completed: task.completed }">
7 <input
8 type="checkbox"
9 :checked="task.completed"
10 @change="$emit('toggle', task.id, $event.target.checked)"
11 >
12 <div class="task-content">
13 <div class="task-title">{{ task.title }}</div>
14 <div v-if="task.description" class="task-description">
15 {{ task.description }}
16 </div>
17 <div class="task-meta">
18 <span class="priority-badge" :class="priorityClass">
19 {{ task.priority }}
20 </span>
21 <span>Created {{ formatDate(task.created_at) }}</span>
22 </div>
23 </div>
24 <div class="task-actions">
25 <button @click="$emit('delete', task.id)" class="delete-btn">
26 Delete
27 </button>
28 </div>
29 </li>
30 `,
31 computed: {
32 priorityClass() {
33 return `priority-${this.task.priority}`;
34 }
35 },
36 methods: {
37 formatDate(dateString) {
38 return new Date(dateString).toLocaleDateString('en-US', {
39 month: 'short',
40 day: 'numeric'
41 });
42 }
43 }
44};
45
46// TaskList component
47const TaskList = {
48 components: { TaskItem },
49 props: ['tasks', 'loading'],
50 emits: ['toggle-task', 'delete-task'],
51 template: `
52 <div v-if="loading" class="loading">
53 <div class="spinner"></div>
54 <p>Loading tasks...</p>
55 </div>
56 <ul v-else-if="tasks.length > 0" class="task-list">
57 <TaskItem
58 v-for="task in tasks"
59 :key="task.id"
60 :task="task"
61 @toggle="$emit('toggle-task', $event)"
62 @delete="$emit('delete-task', $event)"
63 />
64 </ul>
65 <div v-else class="empty-state">
66 <h3>No tasks found</h3>
67 <p>Add your first task to get started!</p>
68 </div>
69 `
70};
71
72// Main app using components
73createApp({
74 components: { TaskList },
75 // ... rest of the app logic
76 template: `
77 <div class="container">
78 <!-- header and form -->
79 <TaskList
80 :tasks="filteredTasks"
81 :loading="isLoading"
82 @toggle-task="toggleTask"
83 @delete-task="deleteTask"
84 />
85 </div>
86 `
87}).mount('#app');
Vue Developer Tools - Your New Best Friend
Once you start using Vue, install the Vue Developer Tools browser extension. It lets you:
- Inspect component hierarchy and see how data flows between components
- Examine reactive data and watch it change in real-time
- Debug performance issues by seeing which components re-render
- Time travel debugging with Vuex (Vue’s state management)
The tools make debugging Vue apps much easier than vanilla JavaScript because you can see exactly which data changed and which components updated.
Lifecycle Hooks - When Things Happen
Vue components have lifecycle hooks that let you run code at specific moments:
1createApp({
2 async mounted() {
3 // Component is ready and mounted to the DOM
4 await this.loadTasks();
5 },
6
7 updated() {
8 // Component has re-rendered after data changes
9 console.log('UI updated');
10 },
11
12 beforeUnmount() {
13 // Component is about to be removed
14 // Clean up timers, event listeners, etc.
15 }
16}).mount('#app');
The most common one is mounted()
- it’s like DOMContentLoaded
but for your Vue component.
Watchers - React to Data Changes
Sometimes you need to perform side effects when data changes:
1watch: {
2 // Watch for changes to currentFilter
3 currentFilter(newFilter, oldFilter) {
4 console.log(`Filter changed from ${oldFilter} to ${newFilter}`);
5 // Could save to localStorage, update URL, etc.
6 },
7
8 // Watch deep changes to tasks array
9 tasks: {
10 handler(newTasks) {
11 localStorage.setItem('tasks', JSON.stringify(newTasks));
12 },
13 deep: true // Watch for changes to objects inside the array
14 }
15}
The Vue Way vs The Manual Way
What Vue gives you over vanilla JavaScript:
Reactivity
- Manual: Update DOM whenever data changes
- Vue: Describe what the UI should look like; Vue updates automatically
Event Handling
- Manual:
addEventListener
and managing event listeners - Vue:
@click="method"
and automatic cleanup
Form Handling
- Manual: Get values from DOM, validate, clear manually
- Vue:
v-model
for two-way binding, automatic validation
Conditional Rendering
- Manual: Show/hide elements with
display
orappendChild
/removeChild
- Vue:
v-if
,v-show
that update automatically
List Rendering
- Manual: Loop through data, create elements, append to DOM
- Vue:
v-for
handles everything including efficient updates
What This Means for Your Career
Vue.js hits the sweet spot for many companies:
- Easier to learn than React or Angular, so teams can onboard faster
- Powerful enough for enterprise applications
- Great ecosystem with Vue Router, Vuex, and excellent tooling
- Flexible - can be adopted incrementally in existing applications
Many companies choose Vue specifically because the learning curve doesn’t kill project timelines. You can be productive on day one, which makes you valuable immediately.
Beyond the Basics
Once you’re comfortable with basic Vue, explore:
- Vue Router for single-page application routing
- Pinia (or Vuex) for complex state management
- Vue CLI or Vite for professional development workflows
- Composition API for better code organization in large apps
The Bottom Line
Vue.js gives you all the power of modern JavaScript frameworks without the complexity overhead. You get reactive data binding, component organization, and a fantastic developer experience while still writing code that feels familiar.
The best part? Everything you learned about HTTP requests, promises, and JavaScript fundamentals in the vanilla version still applies. Vue just makes the UI part dramatically easier.
Ready to see how the industry standard handles the same problem? Let’s rebuild this one more time with React.