Vue.js Fundamentals - Mengapa Vue Ada, Core Concepts, dan Membangun Production Apps

Vue.js Fundamentals - Mengapa Vue Ada, Core Concepts, dan Membangun Production Apps

Kuasai Vue.js dari dasar. Pelajari mengapa Vue diciptakan, pahami core concepts seperti templates, reactive data, computed properties, watchers, components, directives, dan composition API. Bangun complete production-ready expense tracker app yang cover semua Vue fundamentals dengan best practices.

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

Pengenalan

Vue.js telah menjadi salah satu frontend frameworks yang paling approachable dan powerful untuk building interactive user interfaces. Tapi mengapa Vue ada, dan apa yang membuatnya unique? Understanding Vue's philosophy dan core concepts adalah essential untuk building scalable, maintainable web applications.

Dalam artikel ini, kita akan explore Vue's history, understand mengapa Vue diciptakan, dive deep ke core concepts, dan build complete production-ready expense tracker application yang demonstrate semua fundamental Vue patterns.

Mengapa Vue Ada

Problem Sebelum Vue

Sebelum Vue, building interactive web applications memiliki beberapa challenges:

  • DOM Manipulation: Developers manually update DOM menggunakan jQuery atau vanilla JavaScript
  • State Management: Keeping UI in sync dengan application state sangat error-prone
  • Reusability: Components tidak easily reusable across applications
  • Performance: Updating DOM sangat slow dan inefficient
  • Learning Curve: Frameworks seperti React dan Angular memiliki steep learning curves
  • Complexity: Large applications menjadi difficult to maintain

Vue's Solution

Evan You created Vue di 2014 untuk solve problems ini:

  • Progressive Framework: Start simple dan scale up sesuai kebutuhan
  • Reactive Data Binding: Automatic UI updates ketika data changes
  • Component-Based: Build encapsulated components yang manage their own state
  • Gentle Learning Curve: Easy to learn untuk beginners, powerful untuk experts
  • Flexible: Use as much atau as little sesuai kebutuhan
  • Developer Experience: Intuitive API, excellent tooling, dan vibrant ecosystem

Core Concepts

1. Templates

Templates adalah foundation dari Vue components. Mereka use familiar HTML syntax dengan 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 adalah heart dari Vue. Function ref creates reactive data yang automatically updates 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 adalah reactive values yang derived dari other reactive data. Mereka cached dan hanya recompute ketika 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 untuk perform side effects ketika 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 adalah reusable, encapsulated pieces dari UI. Vue uses Single File Components (SFC) untuk 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 dan Emits

Props pass data dari parent ke child. Emits send events dari child ke parent.

Props dan 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 adalah special attributes yang add behavior ke 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 untuk run code di specific stages dari 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

Composition API provides lebih flexible way untuk organize component logic menggunakan 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 untuk pass content dari parent ke 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

Mari kita build complete expense tracker application yang demonstrate semua 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 dan focused
// ✅ Use meaningful component names
// ✅ Separate container dan presentational components
// ✅ Use composition untuk shared logic
// ✅ Leverage TypeScript untuk type safety

2. Reactivity Management

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

3. Performance

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

4. Code Organization

vue
// ✅ Group related logic di composables
// ✅ Use consistent naming conventions
// ✅ Keep components di separate files
// ✅ Use TypeScript untuk type safety
// ✅ Document complex logic dengan comments

5. Testing

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

Common Mistakes & Pitfalls

1. Mutating Props Directly

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

2. Missing Dependency di 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 di 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 di 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 tidak cleaned up
watch(searchQuery, async (newQuery) => {
  const response = await fetch(`/api/search?q=${newQuery}`);
  results.value = await response.json();
});
 
// ✅ Correct - watcher automatically cleaned up di setup
// Vue automatically stops watchers ketika component unmounts

Kesimpulan

Vue.js telah revolutionize frontend development dengan providing progressive, approachable framework yang scales dari simple widgets ke complex applications. Understanding core concepts-nya—templates, reactivity, computed properties, watchers, components, dan Composition API—adalah fundamental untuk menjadi proficient Vue developer.

Expense tracker application yang kita build demonstrate semua concepts ini di action. Dengan mastering fundamentals ini dan following best practices, kamu akan able untuk build scalable, maintainable Vue applications.

Key takeaways:

  1. Vue solve problem dari managing complex UIs dengan reactive data binding
  2. Components adalah building blocks dari Vue applications
  3. Composition API provide flexible way untuk organize logic
  4. Props dan emits enable parent-child communication
  5. Directives dan lifecycle hooks provide powerful tools untuk UI management
  6. Practice building real applications untuk solidify knowledge

Next steps:

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

Vue adalah journey, bukan destination. Keep learning, building, dan improving your skills.


Related Posts