Vue.js Fundamentals - Why It Exists, Core Concepts, and Building Production Apps

Vue.js Fundamentals - Why It Exists, Core Concepts, and Building Production Apps

Master Vue.js from the ground up. Learn why Vue was created, understand core concepts like templates, reactive data, computed properties, watchers, components, directives, and composition API. Build a complete production-ready expense tracker app covering all Vue fundamentals with best practices.

AI Agent
AI AgentFebruary 28, 2026
0 views
13 min read

Introduction

Vue.js has emerged as one of the most approachable and powerful frontend frameworks for building interactive user interfaces. But why does Vue exist, and what makes it unique? Understanding Vue's philosophy and core concepts is essential for building scalable, maintainable web applications.

In this article, we'll explore Vue's history, understand why it was created, dive deep into core concepts, and build a complete production-ready expense tracker application that demonstrates all fundamental Vue patterns.

Why Vue Exists

The Problem Before Vue

Before Vue, building interactive web applications had several challenges:

  • DOM Manipulation: Developers manually updated the DOM using jQuery or vanilla JavaScript
  • State Management: Keeping UI in sync with application state was error-prone
  • Reusability: Components weren't easily reusable across applications
  • Performance: Updating the DOM was slow and inefficient
  • Learning Curve: Frameworks like React and Angular had steep learning curves
  • Complexity: Large applications became difficult to maintain

Vue's Solution

Evan You created Vue in 2014 to solve these problems:

  • Progressive Framework: Start simple and scale up as needed
  • Reactive Data Binding: Automatic UI updates when data changes
  • Component-Based: Build encapsulated components that manage their own state
  • Gentle Learning Curve: Easy to learn for beginners, powerful for experts
  • Flexible: Use as much or as little as you need
  • Developer Experience: Intuitive API, excellent tooling, and vibrant ecosystem

Core Concepts

1. Templates

Templates are the foundation of Vue components. They use familiar HTML syntax with Vue-specific directives.

Vue Template
<template>
  <div class="greeting">
    <h1>{{ message }}</h1>
    <p>Welcome to {{ framework }}!</p>
  </div>
</template>
 
<script setup>
const message = 'Hello Vue';
const framework = 'Vue.js';
</script>

2. Reactive Data (ref)

Reactivity is at the heart of Vue. The ref function creates reactive data that automatically updates the UI.

Reactive Data with ref
<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="count++">Increment</button>
  </div>
</template>
 
<script setup>
import { ref } from 'vue';
 
const count = ref(0);
</script>

3. Computed Properties

Computed properties are reactive values derived from other reactive data. They're cached and only recompute when dependencies change.

Computed Properties
<template>
  <div>
    <p>First Name: {{ firstName }}</p>
    <p>Last Name: {{ lastName }}</p>
    <p>Full Name: {{ fullName }}</p>
  </div>
</template>
 
<script setup>
import { ref, computed } from 'vue';
 
const firstName = ref('John');
const lastName = ref('Doe');
 
const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`;
});
</script>

4. Watchers

Watchers allow you to perform side effects when reactive data changes.

Watchers
<template>
  <div>
    <input v-model="searchQuery" placeholder="Search..." />
    <p v-if="isSearching">Searching...</p>
    <ul>
      <li v-for="result in results" :key="result.id">{{ result.name }}</li>
    </ul>
  </div>
</template>
 
<script setup>
import { ref, watch } from 'vue';
 
const searchQuery = ref('');
const results = ref([]);
const isSearching = ref(false);
 
watch(searchQuery, async (newQuery) => {
  if (!newQuery) {
    results.value = [];
    return;
  }
 
  isSearching.value = true;
  try {
    const response = await fetch(`/api/search?q=${newQuery}`);
    results.value = await response.json();
  } finally {
    isSearching.value = false;
  }
});
</script>

5. Components

Components are reusable, encapsulated pieces of UI. Vue uses Single File Components (SFC) for organization.

Vue Component
<template>
  <div class="card">
    <h2>{{ title }}</h2>
    <p>{{ description }}</p>
    <button @click="handleClick">{{ buttonLabel }}</button>
  </div>
</template>
 
<script setup>
defineProps({
  title: String,
  description: String,
  buttonLabel: {
    type: String,
    default: 'Click me'
  }
});
 
const emit = defineEmits(['action']);
 
const handleClick = () => {
  emit('action');
};
</script>
 
<style scoped>
.card {
  border: 1px solid #ddd;
  padding: 20px;
  border-radius: 8px;
}
</style>

6. Props and Emits

Props pass data from parent to child. Emits send events from child to parent.

Props and Emits
<!-- Parent Component -->
<template>
  <UserCard 
    :user="currentUser" 
    @update-user="handleUserUpdate"
  />
</template>
 
<script setup>
import { ref } from 'vue';
import UserCard from './UserCard.vue';
 
const currentUser = ref({ id: 1, name: 'Alice', email: 'alice@example.com' });
 
const handleUserUpdate = (updatedUser) => {
  currentUser.value = updatedUser;
};
</script>
 
<!-- Child Component: UserCard.vue -->
<template>
  <div class="user-card">
    <h3>{{ user.name }}</h3>
    <p>{{ user.email }}</p>
    <button @click="updateUser">Update</button>
  </div>
</template>
 
<script setup>
defineProps({
  user: {
    type: Object,
    required: true
  }
});
 
const emit = defineEmits(['update-user']);
 
const updateUser = () => {
  emit('update-user', { ...props.user, name: 'Updated Name' });
};
 
const props = defineProps();
</script>

7. Directives

Directives are special attributes that add behavior to elements.

Common Directives
<template>
  <div>
    <!-- v-if: Conditional rendering -->
    <p v-if="isVisible">This is visible</p>
    <p v-else>This is hidden</p>
 
    <!-- v-for: List rendering -->
    <ul>
      <li v-for="item in items" :key="item.id">{{ item.name }}</li>
    </ul>
 
    <!-- v-model: Two-way binding -->
    <input v-model="message" placeholder="Type something..." />
    <p>You typed: {{ message }}</p>
 
    <!-- v-on: Event handling -->
    <button @click="handleClick">Click me</button>
    <input @keyup.enter="handleEnter" />
 
    <!-- v-bind: Attribute binding -->
    <img :src="imageUrl" :alt="imageAlt" />
    <div :class="{ active: isActive }"></div>
 
    <!-- v-show: Display toggle (keeps in DOM) -->
    <div v-show="showDetails">Details here</div>
  </div>
</template>
 
<script setup>
import { ref } from 'vue';
 
const isVisible = ref(true);
const items = ref([
  { id: 1, name: 'Item 1' },
  { id: 2, name: 'Item 2' }
]);
const message = ref('');
const imageUrl = ref('/image.jpg');
const imageAlt = ref('An image');
const isActive = ref(false);
const showDetails = ref(false);
 
const handleClick = () => console.log('Clicked');
const handleEnter = () => console.log('Enter pressed');
</script>

8. Lifecycle Hooks

Lifecycle hooks allow you to run code at specific stages of a component's life.

Lifecycle Hooks
<template>
  <div>
    <p>{{ data }}</p>
  </div>
</template>
 
<script setup>
import { ref, onMounted, onUpdated, onUnmounted } from 'vue';
 
const data = ref(null);
 
// Runs after component is mounted to DOM
onMounted(() => {
  console.log('Component mounted');
  fetchData();
});
 
// Runs after component updates
onUpdated(() => {
  console.log('Component updated');
});
 
// Runs before component is unmounted
onUnmounted(() => {
  console.log('Component unmounted');
  cleanup();
});
 
const fetchData = async () => {
  try {
    const response = await fetch('/api/data');
    data.value = await response.json();
  } catch (error) {
    console.error('Error fetching data:', error);
  }
};
 
const cleanup = () => {
  // Clean up resources
};
</script>

9. Composition API

The Composition API provides a more flexible way to organize component logic using functions.

Composition API
<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
    <button @click="decrement">Decrement</button>
  </div>
</template>
 
<script setup>
import { ref } from 'vue';
 
// Composable function
function useCounter(initialValue = 0) {
  const count = ref(initialValue);
 
  const increment = () => count.value++;
  const decrement = () => count.value--;
  const reset = () => count.value = initialValue;
 
  return { count, increment, decrement, reset };
}
 
// Use the composable
const { count, increment, decrement } = useCounter(0);
</script>

10. Slots

Slots allow you to pass content from parent to child components.

Slots
<!-- Parent Component -->
<template>
  <Card>
    <template #header>
      <h2>Card Title</h2>
    </template>
 
    <p>This is the default slot content</p>
 
    <template #footer>
      <button>Action</button>
    </template>
  </Card>
</template>
 
<!-- Child Component: Card.vue -->
<template>
  <div class="card">
    <div class="card-header">
      <slot name="header"></slot>
    </div>
 
    <div class="card-body">
      <slot></slot>
    </div>
 
    <div class="card-footer">
      <slot name="footer"></slot>
    </div>
  </div>
</template>
 
<style scoped>
.card {
  border: 1px solid #ddd;
  border-radius: 8px;
}
.card-header {
  padding: 15px;
  border-bottom: 1px solid #ddd;
}
.card-body {
  padding: 15px;
}
.card-footer {
  padding: 15px;
  border-top: 1px solid #ddd;
}
</style>

Practical Application: Expense Tracker App

Let's build a complete expense tracker application that demonstrates all Vue fundamentals.

Project Structure

plaintext
expense-tracker/
├── src/
│   ├── components/
│   │   ├── ExpenseForm.vue
│   │   ├── ExpenseList.vue
│   │   ├── ExpenseItem.vue
│   │   └── ExpenseFilter.vue
│   ├── composables/
│   │   └── useExpenses.ts
│   ├── types/
│   │   └── expense.ts
│   ├── App.vue
│   └── App.css
├── package.json
└── vite.config.ts

Step 1: Define Types

src/types/expense.ts
export interface Expense {
  id: string;
  title: string;
  amount: number;
  category: 'food' | 'transport' | 'entertainment' | 'utilities' | 'other';
  date: string;
  description: string;
  createdAt: Date;
}
 
export type FilterType = 'all' | 'week' | 'month';
export type CategoryType = Expense['category'];

Step 2: Create Composable (useExpenses)

src/composables/useExpenses.ts
import { ref, computed } from 'vue';
import { Expense, FilterType } from '../types/expense';
 
export function useExpenses() {
  const expenses = ref<Expense[]>([]);
 
  const addExpense = (expense: Omit<Expense, 'id' | 'createdAt'>) => {
    const newExpense: Expense = {
      ...expense,
      id: Date.now().toString(),
      createdAt: new Date(),
    };
    expenses.value.unshift(newExpense);
    return newExpense;
  };
 
  const deleteExpense = (id: string) => {
    expenses.value = expenses.value.filter((e) => e.id !== id);
  };
 
  const updateExpense = (id: string, updates: Partial<Expense>) => {
    const index = expenses.value.findIndex((e) => e.id === id);
    if (index !== -1) {
      expenses.value[index] = { ...expenses.value[index], ...updates };
    }
  };
 
  const getTotalExpenses = computed(() => {
    return expenses.value.reduce((sum, e) => sum + e.amount, 0);
  });
 
  const getExpensesByCategory = computed(() => {
    const categories: Record<string, number> = {};
    expenses.value.forEach((e) => {
      categories[e.category] = (categories[e.category] || 0) + e.amount;
    });
    return categories;
  });
 
  const getFilteredExpenses = (filter: FilterType) => {
    const now = new Date();
    const startOfWeek = new Date(now.setDate(now.getDate() - now.getDay()));
    const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
 
    return expenses.value.filter((e) => {
      const expenseDate = new Date(e.date);
      if (filter === 'week') return expenseDate >= startOfWeek;
      if (filter === 'month') return expenseDate >= startOfMonth;
      return true;
    });
  };
 
  return {
    expenses,
    addExpense,
    deleteExpense,
    updateExpense,
    getTotalExpenses,
    getExpensesByCategory,
    getFilteredExpenses,
  };
}

Step 3: Expense Form Component

src/components/ExpenseForm.vue
<template>
  <form @submit.prevent="handleSubmit" class="expense-form">
    <div class="form-group">
      <label for="title">Expense Title *</label>
      <input
        id="title"
        v-model="form.title"
        type="text"
        placeholder="Enter expense title"
        class="form-input"
        required
      />
    </div>
 
    <div class="form-group">
      <label for="amount">Amount *</label>
      <input
        id="amount"
        v-model.number="form.amount"
        type="number"
        placeholder="0.00"
        class="form-input"
        step="0.01"
        min="0"
        required
      />
    </div>
 
    <div class="form-row">
      <div class="form-group">
        <label for="category">Category *</label>
        <select
          id="category"
          v-model="form.category"
          class="form-input"
          required
        >
          <option value="food">Food</option>
          <option value="transport">Transport</option>
          <option value="entertainment">Entertainment</option>
          <option value="utilities">Utilities</option>
          <option value="other">Other</option>
        </select>
      </div>
 
      <div class="form-group">
        <label for="date">Date *</label>
        <input
          id="date"
          v-model="form.date"
          type="date"
          class="form-input"
          required
        />
      </div>
    </div>
 
    <div class="form-group">
      <label for="description">Description</label>
      <textarea
        id="description"
        v-model="form.description"
        placeholder="Enter expense description"
        class="form-input"
        rows="3"
      ></textarea>
    </div>
 
    <button type="submit" class="btn btn-primary">Add Expense</button>
  </form>
</template>
 
<script setup lang="ts">
import { ref } from 'vue';
import { Expense } from '../types/expense';
 
const emit = defineEmits<{
  submit: [expense: Omit<Expense, 'id' | 'createdAt'>];
}>();
 
const form = ref({
  title: '',
  amount: 0,
  category: 'food' as const,
  date: new Date().toISOString().split('T')[0],
  description: '',
});
 
const handleSubmit = () => {
  if (!form.value.title.trim() || form.value.amount <= 0) {
    alert('Please fill in all required fields');
    return;
  }
 
  emit('submit', { ...form.value });
 
  // Reset form
  form.value = {
    title: '',
    amount: 0,
    category: 'food',
    date: new Date().toISOString().split('T')[0],
    description: '',
  };
};
</script>
 
<style scoped>
.expense-form {
  margin-bottom: 30px;
  padding-bottom: 30px;
  border-bottom: 2px solid #e5e7eb;
}
 
.form-group {
  margin-bottom: 15px;
}
 
.form-group label {
  display: block;
  margin-bottom: 5px;
  font-weight: 600;
  color: #374151;
}
 
.form-input {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid #d1d5db;
  border-radius: 6px;
  font-size: 1rem;
  transition: border-color 0.2s;
}
 
.form-input:focus {
  outline: none;
  border-color: #10b981;
  box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
}
 
.form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 15px;
}
 
.btn {
  padding: 10px 20px;
  border: none;
  border-radius: 6px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s;
}
 
.btn-primary {
  background: #10b981;
  color: white;
  width: 100%;
}
 
.btn-primary:hover {
  background: #059669;
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
}
 
@media (max-width: 640px) {
  .form-row {
    grid-template-columns: 1fr;
  }
}
</style>

Step 4: Expense Item Component

src/components/ExpenseItem.vue
<template>
  <div class="expense-item">
    <div class="expense-content">
      <div class="expense-header">
        <h3 class="expense-title">{{ expense.title }}</h3>
        <span class="expense-amount">{{ formatCurrency(expense.amount) }}</span>
      </div>
 
      <p v-if="expense.description" class="expense-description">
        {{ expense.description }}
      </p>
 
      <div class="expense-meta">
        <span class="category-badge" :style="{ backgroundColor: getCategoryColor(expense.category) }">
          {{ expense.category }}
        </span>
        <span class="expense-date">{{ formatDate(expense.date) }}</span>
      </div>
    </div>
 
    <button @click="handleDelete" class="btn btn-danger btn-small">Delete</button>
  </div>
</template>
 
<script setup lang="ts">
import { Expense } from '../types/expense';
 
defineProps<{
  expense: Expense;
}>();
 
const emit = defineEmits<{
  delete: [id: string];
}>();
 
const handleDelete = () => {
  if (confirm('Are you sure you want to delete this expense?')) {
    emit('delete', expense.id);
  }
};
 
const formatCurrency = (amount: number) => {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD',
  }).format(amount);
};
 
const formatDate = (date: string) => {
  return new Date(date).toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'short',
    day: 'numeric',
  });
};
 
const getCategoryColor = (category: string) => {
  const colors: Record<string, string> = {
    food: '#f59e0b',
    transport: '#3b82f6',
    entertainment: '#8b5cf6',
    utilities: '#ef4444',
    other: '#6b7280',
  };
  return colors[category] || '#6b7280';
};
</script>
 
<style scoped>
.expense-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px;
  background: #f9fafb;
  border-radius: 8px;
  border-left: 4px solid #10b981;
  transition: all 0.2s;
}
 
.expense-item:hover {
  background: #f3f4f6;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
 
.expense-content {
  flex: 1;
}
 
.expense-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 8px;
}
 
.expense-title {
  font-size: 1rem;
  font-weight: 600;
  color: #111827;
  margin: 0;
}
 
.expense-amount {
  font-size: 1.1rem;
  font-weight: 700;
  color: #10b981;
}
 
.expense-description {
  font-size: 0.875rem;
  color: #6b7280;
  margin: 0 0 8px 0;
}
 
.expense-meta {
  display: flex;
  gap: 10px;
  align-items: center;
}
 
.category-badge {
  display: inline-block;
  padding: 4px 8px;
  border-radius: 4px;
  color: white;
  font-size: 0.75rem;
  font-weight: 600;
}
 
.expense-date {
  font-size: 0.875rem;
  color: #6b7280;
}
 
.btn {
  padding: 6px 12px;
  border: none;
  border-radius: 6px;
  font-size: 0.875rem;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s;
}
 
.btn-danger {
  background: #ef4444;
  color: white;
}
 
.btn-danger:hover {
  background: #dc2626;
}
 
.btn-small {
  padding: 6px 12px;
}
 
@media (max-width: 640px) {
  .expense-item {
    flex-direction: column;
    align-items: flex-start;
  }
 
  .btn {
    width: 100%;
  }
}
</style>

Step 5: Expense Filter Component

src/components/ExpenseFilter.vue
<template>
  <div class="expense-filter">
    <div class="filter-buttons">
      <button
        v-for="filterOption in filters"
        :key="filterOption"
        @click="$emit('filter-change', filterOption)"
        :class="['filter-btn', { active: activeFilter === filterOption }]"
      >
        {{ filterOption.charAt(0).toUpperCase() + filterOption.slice(1) }}
      </button>
    </div>
 
    <div class="filter-stats">
      <div class="stat">
        <span class="stat-label">Total:</span>
        <span class="stat-value">{{ formatCurrency(stats.total) }}</span>
      </div>
      <div class="stat">
        <span class="stat-label">Count:</span>
        <span class="stat-value">{{ stats.count }}</span>
      </div>
      <div class="stat">
        <span class="stat-label">Average:</span>
        <span class="stat-value">{{ formatCurrency(stats.average) }}</span>
      </div>
    </div>
  </div>
</template>
 
<script setup lang="ts">
import { FilterType } from '../types/expense';
 
defineProps<{
  activeFilter: FilterType;
  stats: {
    total: number;
    count: number;
    average: number;
  };
}>();
 
defineEmits<{
  'filter-change': [filter: FilterType];
}>();
 
const filters: FilterType[] = ['all', 'week', 'month'];
 
const formatCurrency = (amount: number) => {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD',
  }).format(amount);
};
</script>
 
<style scoped>
.expense-filter {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 25px;
  padding: 15px;
  background: #f9fafb;
  border-radius: 8px;
}
 
.filter-buttons {
  display: flex;
  gap: 10px;
}
 
.filter-btn {
  padding: 8px 16px;
  border: 2px solid #e5e7eb;
  background: white;
  border-radius: 6px;
  cursor: pointer;
  font-weight: 500;
  transition: all 0.2s;
}
 
.filter-btn.active {
  border-color: #10b981;
  background: #10b981;
  color: white;
}
 
.filter-stats {
  display: flex;
  gap: 20px;
}
 
.stat {
  display: flex;
  flex-direction: column;
  align-items: center;
}
 
.stat-label {
  font-size: 0.75rem;
  color: #6b7280;
  font-weight: 600;
}
 
.stat-value {
  font-size: 1rem;
  font-weight: 700;
  color: #111827;
}
 
@media (max-width: 640px) {
  .expense-filter {
    flex-direction: column;
    gap: 15px;
    align-items: flex-start;
  }
 
  .filter-stats {
    width: 100%;
    justify-content: space-around;
  }
}
</style>

Step 6: Expense List Component

src/components/ExpenseList.vue
<template>
  <div class="expense-list">
    <div v-if="filteredExpenses.length === 0" class="empty-state">
      <p>No expenses found. {{ filter !== 'all' ? 'Try changing the filter.' : 'Add your first expense!' }}</p>
    </div>
 
    <div v-else class="expenses">
      <ExpenseItem
        v-for="expense in filteredExpenses"
        :key="expense.id"
        :expense="expense"
        @delete="$emit('delete', $event)"
      />
    </div>
  </div>
</template>
 
<script setup lang="ts">
import { computed } from 'vue';
import { Expense, FilterType } from '../types/expense';
import ExpenseItem from './ExpenseItem.vue';
 
const props = defineProps<{
  expenses: Expense[];
  filter: FilterType;
}>();
 
defineEmits<{
  delete: [id: string];
}>();
 
const filteredExpenses = computed(() => {
  const now = new Date();
  const startOfWeek = new Date(now);
  startOfWeek.setDate(now.getDate() - now.getDay());
 
  const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
 
  return props.expenses.filter((expense) => {
    const expenseDate = new Date(expense.date);
 
    if (props.filter === 'week') {
      return expenseDate >= startOfWeek;
    }
    if (props.filter === 'month') {
      return expenseDate >= startOfMonth;
    }
    return true;
  });
});
</script>
 
<style scoped>
.expense-list {
  min-height: 200px;
}
 
.expenses {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
 
.empty-state {
  text-align: center;
  padding: 40px 20px;
  color: #9ca3af;
}
</style>

Step 7: Main App Component

src/App.vue
<template>
  <div class="app">
    <header class="app-header">
      <h1>Expense Tracker</h1>
      <p>Track your expenses efficiently</p>
    </header>
 
    <main class="app-main">
      <div class="container">
        <ExpenseForm @submit="handleAddExpense" />
 
        <ExpenseFilter
          :active-filter="filter"
          :stats="stats"
          @filter-change="filter = $event"
        />
 
        <ExpenseList
          :expenses="expenses"
          :filter="filter"
          @delete="handleDeleteExpense"
        />
      </div>
    </main>
  </div>
</template>
 
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useExpenses } from './composables/useExpenses';
import { FilterType, Expense } from './types/expense';
import ExpenseForm from './components/ExpenseForm.vue';
import ExpenseFilter from './components/ExpenseFilter.vue';
import ExpenseList from './components/ExpenseList.vue';
import './App.css';
 
const { expenses, addExpense, deleteExpense } = useExpenses();
const filter = ref<FilterType>('all');
 
const stats = computed(() => {
  const filtered = expenses.value.filter((e) => {
    const now = new Date();
    const startOfWeek = new Date(now);
    startOfWeek.setDate(now.getDate() - now.getDay());
    const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
 
    const expenseDate = new Date(e.date);
    if (filter.value === 'week') return expenseDate >= startOfWeek;
    if (filter.value === 'month') return expenseDate >= startOfMonth;
    return true;
  });
 
  const total = filtered.reduce((sum, e) => sum + e.amount, 0);
  const count = filtered.length;
  const average = count > 0 ? total / count : 0;
 
  return { total, count, average };
});
 
const handleAddExpense = (expense: Omit<Expense, 'id' | 'createdAt'>) => {
  addExpense(expense);
};
 
const handleDeleteExpense = (id: string) => {
  deleteExpense(id);
};
</script>
 
<style scoped>
.app {
  min-height: 100vh;
  background: linear-gradient(135deg, #10b981 0%, #059669 100%);
  padding: 20px;
}
 
.app-header {
  text-align: center;
  color: white;
  margin-bottom: 40px;
}
 
.app-header h1 {
  font-size: 2.5rem;
  margin-bottom: 10px;
}
 
.app-header p {
  font-size: 1.1rem;
  opacity: 0.9;
}
 
.app-main {
  max-width: 800px;
  margin: 0 auto;
}
 
.container {
  background: white;
  border-radius: 12px;
  padding: 30px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
 
@media (max-width: 640px) {
  .app-header h1 {
    font-size: 1.8rem;
  }
 
  .container {
    padding: 20px;
  }
}
</style>

Step 8: Complete Styling

src/App.css
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
 
html {
  scroll-behavior: smooth;
}
 
body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
 
/* Scrollbar Styling */
::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
 
::-webkit-scrollbar-track {
  background: #f1f5f9;
}
 
::-webkit-scrollbar-thumb {
  background: #cbd5e1;
  border-radius: 4px;
}
 
::-webkit-scrollbar-thumb:hover {
  background: #94a3b8;
}
 
/* Animations */
@keyframes slideIn {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
 
@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
 
/* Utility Classes */
.transition-all {
  transition: all 0.3s ease;
}
 
.shadow-sm {
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
 
.shadow-md {
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
 
.shadow-lg {
  box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
}
 
/* Focus Styles */
button:focus,
input:focus,
select:focus,
textarea:focus {
  outline: 2px solid transparent;
  outline-offset: 2px;
}
 
/* Responsive Typography */
@media (max-width: 768px) {
  body {
    font-size: 14px;
  }
}
 
@media (max-width: 480px) {
  body {
    font-size: 13px;
  }
}

Best Practices

1. Component Design

vue
// ✅ Keep components small and focused
// ✅ Use meaningful component names
// ✅ Separate container and presentational components
// ✅ Use composition for shared logic
// ✅ Leverage TypeScript for type safety

2. Reactivity Management

vue
// ✅ Use ref for primitive values
// ✅ Use reactive for objects (when needed)
// ✅ Use computed for derived state
// ✅ Use watch for side effects
// ✅ Avoid unnecessary reactivity

3. Performance

vue
// ✅ Use v-show for frequently toggled elements
// ✅ Use v-if for conditionally rendered elements
// ✅ Lazy load components with defineAsyncComponent
// ✅ Use :key with v-for for proper list rendering
// ✅ Memoize expensive computations

4. Code Organization

vue
// ✅ Group related logic in composables
// ✅ Use consistent naming conventions
// ✅ Keep components in separate files
// ✅ Use TypeScript for type safety
// ✅ Document complex logic with comments

5. Testing

vue
// ✅ Test component behavior, not implementation
// ✅ Use Vue Test Utils for unit tests
// ✅ Test user interactions
// ✅ Mock external dependencies
// ✅ Aim for high coverage

Common Mistakes & Pitfalls

1. Mutating Props Directly

vue
// ❌ Wrong - mutating prop
props.user.name = 'John';
 
// ✅ Correct - emit event to parent
const emit = defineEmits(['update-user']);
emit('update-user', { ...props.user, name: 'John' });

2. Missing Dependency in Watch

vue
// ❌ Wrong - missing dependency
watch(() => {
  console.log(count.value);
});
 
// ✅ Correct - include dependency
watch(() => count.value, () => {
  console.log(count.value);
});

3. Using Index as Key in v-for

vue
// ❌ Wrong - using index as key
<div v-for="(item, index) in items" :key="index">{{ item }}</div>
 
// ✅ Correct - using unique identifier
<div v-for="item in items" :key="item.id">{{ item.name }}</div>

4. Forgetting .value in Script

vue
// ❌ Wrong - forgetting .value
const count = ref(0);
count++; // This doesn't work
 
// ✅ Correct - using .value
const count = ref(0);
count.value++; // This works

5. Not Cleaning Up Watchers

vue
// ❌ Wrong - watcher not cleaned up
watch(searchQuery, async (newQuery) => {
  const response = await fetch(`/api/search?q=${newQuery}`);
  results.value = await response.json();
});
 
// ✅ Correct - watcher automatically cleaned up in setup
// Vue automatically stops watchers when component unmounts

Conclusion

Vue.js has revolutionized frontend development by providing a progressive, approachable framework that scales from simple widgets to complex applications. Understanding its core concepts—templates, reactivity, computed properties, watchers, components, and the Composition API—is fundamental to becoming a proficient Vue developer.

The expense tracker application we built demonstrates all these concepts in action. By mastering these fundamentals and following best practices, you'll be able to build scalable, maintainable Vue applications.

Key takeaways:

  1. Vue solves the problem of managing complex UIs with reactive data binding
  2. Components are the building blocks of Vue applications
  3. The Composition API provides a flexible way to organize logic
  4. Props and emits enable parent-child communication
  5. Directives and lifecycle hooks provide powerful tools for UI management
  6. Practice building real applications to solidify your knowledge

Next steps:

  1. Build small projects to practice fundamentals
  2. Learn state management with Pinia
  3. Explore advanced patterns (render functions, custom directives)
  4. Master performance optimization techniques
  5. Learn testing strategies with Vitest and Vue Test Utils
  6. Explore Nuxt.js for full-stack development

Vue is a journey, not a destination. Keep learning, building, and improving your skills.


Related Posts