Build an Expense Tracker with Vue 3 and Pinia

Build an Expense Tracker with Vue 3 and Pinia

Build an Expense Tracker with Vue 3 and Pinia

Build a practical expense tracker from scratch that demonstrates real-world Vue 3 patterns including Pinia stores, computed totals, and Chart.js integration.

Features

  • Add income and expense transactions with categories
  • Real-time balance, total income, and total expense summaries
  • Doughnut chart breakdown by category
  • Filter by date range and category
  • Persistent state via Pinia + localStorage

Step 1 — Project Setup

npm create vue@latest expense-tracker
cd expense-tracker
npm install pinia chart.js vue-chartjs

Step 2 — Transaction Store

// stores/transactions.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useTransactionStore = defineStore('transactions', () => {
  const transactions = ref([])

  const income  = computed(() =>
    transactions.value.filter(t => t.amount > 0).reduce((s, t) => s + t.amount, 0)
  )
  const expenses = computed(() =>
    transactions.value.filter(t => t.amount < 0).reduce((s, t) => s + t.amount, 0)
  )
  const balance = computed(() => income.value + expenses.value)

  function addTransaction(transaction) {
    transactions.value.push({
      id:        crypto.randomUUID(),
      createdAt: new Date().toISOString(),
      ...transaction,
    })
  }

  function removeTransaction(id) {
    transactions.value = transactions.value.filter(t => t.id !== id)
  }

  return { transactions, income, expenses, balance, addTransaction, removeTransaction }
}, { persist: true })

Step 3 — Transaction Form

<script setup>
import { reactive } from 'vue'
import { useTransactionStore } from '@/stores/transactions'

const store = useTransactionStore()
const form  = reactive({ description: '', amount: '', category: 'food', type: 'expense' })

function submit() {
  store.addTransaction({
    description: form.description,
    amount:      form.type === 'expense' ? -Math.abs(form.amount) : Math.abs(form.amount),
    category:    form.category,
  })
  Object.assign(form, { description: '', amount: '' })
}
</script>

Step 4 — Chart Component

<script setup>
import { computed } from 'vue'
import { Doughnut } from 'vue-chartjs'
import { useTransactionStore } from '@/stores/transactions'

const store = useTransactionStore()

const chartData = computed(() => {
  const categories = {}
  store.transactions
    .filter(t => t.amount < 0)
    .forEach(t => { categories[t.category] = (categories[t.category] || 0) + Math.abs(t.amount) })
  return {
    labels:   Object.keys(categories),
    datasets: [{ data: Object.values(categories), backgroundColor: ['#FF6384','#36A2EB','#FFCE56','#4BC0C0'] }]
  }
})
</script>
All Comments