Vue.js - The Gentle Introduction to Modern Frameworks

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 with v-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 or appendChild/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.