Vue.js 3 and the Composition API: Building an Expense Tracker - An Educational Guide
This chapter will guide you through the process of building a simple expense tracker application using Vue.js 3, specifically leveraging the Composition API and the script setup syntax. This project, originally part of a “20 Vanilla JavaScript Projects” course and later adapted for React, serves as an excellent practical example for understanding modern Vue.js development.
1. Introduction to Vue.js 3 and the Composition API
Vue.js is a progressive JavaScript framework for building user interfaces. Known for its ease of use and flexibility, Vue.js has become a popular choice for front-end development. Version 3 of Vue.js introduced significant improvements, most notably the Composition API.
Vue.js: A progressive JavaScript framework used for building user interfaces and single-page applications. It is designed to be incrementally adoptable and focuses on the view layer.
Traditionally, Vue.js components were structured using the Options API.
Options API: The original way to structure Vue.js components, where component logic is organized into options like
data
,methods
,computed
, andwatch
. This approach groups related logic by option type.
However, Vue.js 3 emphasizes the Composition API as a more organized and efficient way to manage component logic.
1.1 Understanding the Composition API
The Composition API is a set of APIs that allows you to author Vue.js components using imported functions instead of declaring options. It aims to improve code reusability, readability, and especially reactivity.
Reactivity: In front-end frameworks, reactivity refers to the automatic updating of the user interface (UI) when the underlying data changes. This eliminates the need for manual DOM manipulation.
Key Benefits of the Composition API:
- Improved Logic Organization: The Composition API allows you to group related logic together, regardless of the option type. This leads to more maintainable and understandable code, especially in complex components.
- Enhanced Reusability: Logic can be extracted into reusable functions (composables), promoting code sharing across components.
- Better Readability: Code becomes more linear and easier to follow as logic is organized by feature rather than option.
- Simplified Reactivity: The Composition API provides straightforward ways to establish reactivity, making UI updates more predictable and efficient.
The Composition API contrasts with the Options API by moving away from organizing code by options (like data
, methods
, computed
) and instead focusing on organizing code by logical features. This approach often results in cleaner and more scalable component structures.
1.2 Project Overview: Expense Tracker Application
This project involves building a web application to track income and expenses. The application will feature:
- Total Balance Display: Shows the overall financial balance.
- Income and Expense Balances: Displays separate balances for total income and total expenses.
- Transaction History: A list of all transactions, visually differentiated as income (green border) or expense (red border). Each transaction will include a delete option.
- Add Transaction Form: A form to input new income or expense transactions with descriptions and amounts.
- Local Storage Persistence: Data will be saved in the browser’s local storage, ensuring data persistence across sessions.
- Toast Notifications: Using View Toastify to provide user feedback upon adding or deleting transactions.
View Toastify: A Vue.js library used for displaying elegant and non-intrusive notification messages (toasts) to the user.
This project provides a practical context to learn and apply the concepts of Vue.js 3 and the Composition API.
2. Project Setup
To begin, ensure you have Node.js and npm (Node Package Manager) installed on your system. These are essential tools for JavaScript development and managing project dependencies.
Node.js: A JavaScript runtime environment that allows you to run JavaScript code outside of a web browser. It is essential for modern web development tooling.
npm (Node Package Manager): A package manager for JavaScript. It is used to install, manage, and share JavaScript packages and libraries.
2.1 Creating a New Vue.js Project with Vue CLI
We will use the Vue CLI (Command Line Interface) to quickly scaffold a new Vue.js project. If you don’t have Vue CLI installed globally, you can use npx
to run it directly without global installation.
Vue CLI (Command Line Interface): A command-line tool for scaffolding and managing Vue.js projects. It simplifies project setup and development workflows.
Open your terminal and navigate to the directory where you want to create your project. Then, run the following command:
npx create-vue@latest .
The .
specifies that you want to create the project in the current directory. You will be prompted with a series of questions:
- Project name: Choose a name for your project (e.g.,
vue-expense-tracker
). - Add TypeScript? Select
No
. - Add JSX Support? Select
No
. - Add Vue Router for Single Page Application development? Select
No
(for this project, a router is not needed). - Add Pinia for state management? Select
No
(for this project, Pinia is not necessary). - Add Vitest for Unit testing? Select
No
. - Add Cypress for E2E testing? Select
No
. - Add ESLint for code linting? Select
No
.
After answering these questions, the Vue CLI will generate the basic project structure. Navigate into your project directory (if you created a subdirectory project) and install the project dependencies using npm:
npm install
To start the development server and see your application in the browser, run:
npm run dev
This will launch a development server, typically accessible at http://localhost:5173
(the port number may vary).
2.2 File Structure Overview
Let’s examine the key files and folders in the generated Vue.js project:
-
package.json
: This file contains metadata about your project, including dependencies (libraries your project relies on) and scripts (commands to run development tasks). You’ll seevue
listed underdependencies
andvite
underdevDependencies
.package.json: A JSON file at the root of a Node.js project that describes the project’s dependencies, scripts, and other metadata. It is essential for managing project dependencies and build processes.
-
vite.config.js
: Configuration file for Vite, the development server and build tool used by Vue.js. You can customize development server settings here if needed, like changing the port number.Vite: A fast build tool and development server for modern web development. It provides features like hot module replacement and optimized builds.
-
index.html
: The main HTML file for your Single Page Application (SPA). It includes a<div id="app">
where your Vue.js application will be mounted.Single Page Application (SPA): A web application that loads a single HTML page and dynamically updates the content as the user interacts with it, without requiring full page reloads.
-
src/main.js
: The entry point for your Vue.js application. It initializes the Vue application, mounts the main component (App.vue
), and imports global CSS. -
src/App.vue
: The root component of your application. It serves as the container for all other components and defines the overall application structure. -
src/assets/
: Directory to store assets like CSS files and images. -
src/components/
: Directory to store reusable Vue.js components. -
public/
: Directory for public assets likefavicon.ico
which are served directly without processing by Vite.
2.3 Cleaning Up and Setting Up Basic Styles
To start with a clean slate, perform the following cleanup:
-
Delete files in
src/assets
: Deletebase.css
,logo.svg
, andmain.css
. -
Delete files in
src/components
: DeleteHelloWorld.vue
andicons/
. -
Modify
src/App.vue
: Remove all existing content within the<template>
and<script>
tags. Replace the content with:<template> <div>My App</div> </template> <script setup> </script> <style scoped> </style>
-
Create
src/assets/style.css
: Create a new file namedstyle.css
in thesrc/assets
folder. Copy the CSS styles provided in the transcript (or from the linked GitHub repository) and paste them into this file. -
Import
style.css
insrc/main.js
: Modifysrc/main.js
to import your newly created CSS file:import './assets/style.css' import { createApp } from 'vue' import App from './App.vue' createApp(App).mount('#app')
-
Replace
public/favicon.ico
(Optional): You can replace the default favicon with a custom one as mentioned in the transcript, or keep the default.
After these steps, your basic Vue.js application should be set up with a basic structure and styling applied. Running npm run dev
should display “My App” centered on the screen with the styles from style.css
.
3. Component Breakdown
To structure the expense tracker application effectively, we will break down the UI into reusable components. This modular approach makes development more organized and maintainable. Based on the application’s design, we will create the following components:
Header.vue
: Displays the main title of the application (“Expense Tracker”).Balance.vue
: Shows the total balance.IncomeExpenses.vue
: Displays the income and expense balances side-by-side.TransactionList.vue
: Renders the list of transactions, including income and expenses.AddTransaction.vue
: Contains the form to add new transactions.
Create these component files within the src/components
directory. Each file will have a .vue
extension and should use PascalCase naming convention (e.g., Header.vue
, Balance.vue
).
3.1 Creating Components and Basic Structure
For each component file (Header.vue
, Balance.vue
, IncomeExpenses.vue
, TransactionList.vue
, AddTransaction.vue
), add the basic Vue.js component structure:
<template>
<div>
<!-- Component Content Here -->
</div>
</template>
<script setup>
// Component Logic Here (Composition API)
</script>
<style scoped>
/* Component Specific Styles Here (optional for now) */
</style>
Populating Components with Basic HTML:
Refer to the transcript or the provided GitHub repository to copy the basic HTML structure for each component. Place this HTML within the <template>
section of each component file. For example, in Header.vue
, it will be a simple <h2>
tag. In Balance.vue
, it will be an <h4>
and an <h1>
for the balance amount.
Importing and Registering Components in App.vue
:
To use these components in your main application, you need to import and register them in App.vue
. Using the <script setup>
syntax, component registration is implicit. Simply import the components at the top of App.vue
and then use them in the <template>
section.
<template>
<div class="container">
<Header />
<Balance />
<IncomeExpenses />
<TransactionList />
<AddTransaction />
</div>
</template>
<script setup>
import Header from './components/Header.vue';
import Balance from './components/Balance.vue';
import IncomeExpenses from './components/IncomeExpenses.vue';
import TransactionList from './components/TransactionList.vue';
import AddTransaction from './components/AddTransaction.vue';
</script>
Ensure you add a div
with the class container
in App.vue
to match the CSS styling from style.css
.
After completing these steps, your application should display the basic layout of the expense tracker, with hardcoded text within each component. The next step is to implement the dynamic functionality and data handling.
4. Implementing Core Functionality
Now we will focus on making the application dynamic and interactive. This involves:
- Displaying transactions in the
TransactionList
component. - Calculating and displaying the balance in the
Balance
component. - Calculating and displaying income and expenses in the
IncomeExpenses
component. - Implementing the “Add Transaction” form functionality.
- Adding the “Delete Transaction” functionality.
4.1 Displaying Transactions in TransactionList.vue
We’ll start by displaying a list of transactions in the TransactionList
component. For now, we will use hardcoded data in App.vue
and pass it down to TransactionList.vue
as props.
Props (Properties): Custom attributes you can register on a component. Props allow you to pass data from parent components to child components.
1. Define Transaction Data in App.vue
:
In the <script setup>
section of App.vue
, define a reactive array of transaction objects using ref
from Vue.js.
<script setup>
import { ref } from 'vue';
// ... other imports
const transactions = ref([
{ id: 1, text: 'Flower', amount: -20 },
{ id: 2, text: 'Salary', amount: 300 },
{ id: 3, text: 'Book', amount: -10 },
{ id: 4, text: 'Camera', amount: 150 }
]);
</script>
2. Pass Transactions as Props to TransactionList.vue
:
In App.vue
’s template, bind the transactions
ref as a prop to the <TransactionList>
component.
<template>
<div class="container">
<Header />
<Balance />
<IncomeExpenses />
<TransactionList :transactions="transactions" /> <--- Pass as prop
<AddTransaction />
</div>
</template>
3. Define Props in TransactionList.vue
:
In TransactionList.vue
, use defineProps
to declare the transactions
prop.
<script setup>
import { defineProps } from 'vue';
const props = defineProps({
transactions: {
type: Array,
required: true
}
});
</script>
4. Render Transactions in TransactionList.vue
Template:
Use v-for
directive to loop through the transactions
prop and render a list item (<li>
) for each transaction. Use v-bind:key
for efficient list rendering, and v-bind:class
to conditionally apply minus
or plus
class based on the transaction amount.
<template>
<div>
<h3>History</h3>
<ul id="list" class="list">
<li v-for="transaction in props.transactions" :key="transaction.id" :class="{ minus: transaction.amount < 0, plus: transaction.amount > 0 }">
{{ transaction.text }} <span>{{ transaction.amount < 0 ? '-' : '+' }}${{ Math.abs(transaction.amount) }}</span><button class="delete-btn">x</button>
</li>
</ul>
</div>
</template>
Now, your application should display the hardcoded transactions in the transaction list. The border color (red or green) and +/- sign will dynamically change based on the amount.
4.2 Calculating and Displaying Balance in Balance.vue
The Balance
component needs to calculate the total balance based on the transactions. We’ll use a computed property in App.vue
to calculate the total and pass it as a prop to Balance.vue
.
Computed Properties: Properties that are derived from existing data. They are cached and only re-evaluated when their dependencies change, making them efficient for complex calculations.
1. Create a Computed Property for Total Balance in App.vue
:
Import computed
from Vue.js and define a computed property called total
that calculates the sum of all transaction amounts.
<script setup>
import { ref, computed } from 'vue'; // Import computed
// ... transactions ref ...
const total = computed(() => {
return props.transactions.value.reduce((acc, transaction) => acc + transaction.amount, 0);
});
</script>
2. Pass Total as Prop to Balance.vue
:
In App.vue
’s template, bind the total
computed property as a prop to the <Balance>
component.
<template>
<div class="container">
<Header />
<Balance :total="total" /> <--- Pass as prop
<IncomeExpenses />
<TransactionList :transactions="transactions" />
<AddTransaction />
</div>
</template>
3. Define Props in Balance.vue
:
In Balance.vue
, use defineProps
to declare the total
prop.
<script setup>
import { defineProps } from 'vue';
const props = defineProps({
total: {
type: Number,
required: true
}
});
</script>
4. Display Total in Balance.vue
Template:
In Balance.vue
’s template, replace the hardcoded balance with the total
prop value.
<template>
<div>
<h4>Your Balance</h4>
<h1>${{ props.total.toFixed(2) }}</h1>
</div>
</template>
Now, the Balance
component should dynamically display the calculated total balance based on the transactions.
4.3 Calculating and Displaying Income and Expenses in IncomeExpenses.vue
Similar to the balance, we will calculate income and expense totals using computed properties in App.vue
and pass them as props to IncomeExpenses.vue
.
1. Create Computed Properties for Income and Expenses in App.vue
:
Define two computed properties, income
and expenses
, that filter transactions based on whether the amount is positive (income) or negative (expense) and then calculate the sum.
<script setup>
import { ref, computed } from 'vue';
// ... transactions ref ...
// ... total computed property ...
const income = computed(() => {
return props.transactions.value
.filter(transaction => transaction.amount > 0)
.reduce((acc, transaction) => acc + transaction.amount, 0)
.toFixed(2);
});
const expenses = computed(() => {
return props.transactions.value
.filter(transaction => transaction.amount < 0)
.reduce((acc, transaction) => acc + transaction.amount, 0)
.toFixed(2);
});
</script>
2. Pass Income and Expenses as Props to IncomeExpenses.vue
:
In App.vue
’s template, bind the income
and expenses
computed properties as props to the <IncomeExpenses>
component.
<template>
<div class="container">
<Header />
<Balance :total="total" />
<IncomeExpenses :income="income" :expenses="expenses" /> <--- Pass as props
<TransactionList :transactions="transactions" />
<AddTransaction />
</div>
</template>
3. Define Props in IncomeExpenses.vue
:
In IncomeExpenses.vue
, use defineProps
to declare the income
and expenses
props.
<script setup>
import { defineProps } from 'vue';
const props = defineProps({
income: {
type: Number, // Corrected type to Number as we are passing numbers
required: true
},
expenses: {
type: Number, // Corrected type to Number
required: true
}
});
</script>
4. Display Income and Expenses in IncomeExpenses.vue
Template:
In IncomeExpenses.vue
’s template, replace the hardcoded income and expense values with the income
and expenses
prop values.
<template>
<div class="inc-exp-container">
<div>
<h4>Income</h4>
<p id="money-plus" class="money plus">+${{ props.income }}</p>
</div>
<div>
<h4>Expense</h4>
<p id="money-minus" class="money minus">-${{ props.expenses }}</p>
</div>
</div>
</template>
Now, the IncomeExpenses
component should dynamically display the calculated income and expense balances.
4.4 Implementing “Add Transaction” Form Functionality in AddTransaction.vue
The AddTransaction
component needs to handle user input from the form and emit an event to App.vue
when a new transaction is submitted.
1. Set up Form and Input Binding in AddTransaction.vue
Template:
Use v-model
to bind the input fields (text
and amount
) to reactive refs in the <script setup>
section. Add an @submit.prevent
listener to the <form>
to handle form submission and prevent page reload.
<template>
<div>
<h3>Add new transaction</h3>
<form id="form" @submit.prevent="onSubmit">
<div class="form-control">
<label for="text">Text</label>
<input type="text" id="text" v-model="text" placeholder="Enter text..." />
</div>
<div class="form-control">
<label for="amount">Amount <br />
(negative - expense, positive - income)</label>
<input type="text" id="amount" v-model="amount" placeholder="Enter amount..." />
</div>
<button class="btn">Add transaction</button>
</form>
</div>
</template>
2. Define Reactive Refs and onSubmit
Function in AddTransaction.vue
Script:
Import ref
and defineEmits
from Vue.js. Create reactive refs text
and amount
to store input values. Define an onSubmit
function that handles form submission, performs basic validation, emits a custom event transactionSubmitted
with transaction data, and clears the input fields.
<script setup>
import { ref, defineEmits } from 'vue';
import { useToast } from 'vue-toastify'; // Import useToast
const text = ref('');
const amount = ref('');
const emit = defineEmits(['transactionSubmitted']); // Define custom event
const toast = useToast(); // Initialize toast
const onSubmit = () => {
if (!text.value.trim() || !amount.value.trim()) {
toast.error("Both fields must be filled"); // Use toast for error message
return;
}
const transactionData = {
text: text.value,
amount: parseFloat(amount.value) // Parse amount as float
};
emit('transactionSubmitted', transactionData); // Emit custom event
text.value = ''; // Clear input fields
amount.value = '';
};
</script>
3. Install and Configure View Toastify:
Install vue-toastify
using npm:
npm install vue-toastify@next
In src/main.js
, import toast
and its CSS, and use it in your Vue app.
import './assets/style.css'
import 'vue-toastify/listner.css'; // Corrected import path
import { createApp } from 'vue'
import App from './App.vue'
import { toast } from 'vue-toastify'; // Import toast
const app = createApp(App);
app.use(toast); // Use toast plugin
app.mount('#app');
4. Handle transactionSubmitted
Event in App.vue
:
In App.vue
’s template, listen for the transactionSubmitted
custom event on the <AddTransaction>
component. Create a handler function handleTransactionSubmitted
in App.vue
’s script to receive the transaction data, generate a unique ID, add the new transaction to the transactions
array, and display a success toast.
<template>
<div class="container">
<Header />
<Balance :total="total" />
<IncomeExpenses :income="income" :expenses="expenses" />
<TransactionList :transactions="transactions" />
<AddTransaction @transactionSubmitted="handleTransactionSubmitted" /> <--- Listen for event
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { useToast } from 'vue-toastify'; // Import useToast
// ... other imports
const transactions = ref([ /* ... initial transactions ... */ ]);
// ... total, income, expenses computed properties ...
const toast = useToast(); // Initialize toast
const handleTransactionSubmitted = (transactionData) => {
const newTransaction = {
id: generateUniqueId(), // Function to generate unique ID (implement below)
...transactionData // Spread operator to include text and amount
};
transactions.value.push(newTransaction);
toast.success("Transaction added"); // Success toast
};
function generateUniqueId() { // Simple unique ID generator
return Math.floor(Math.random() * 1000000);
}
</script>
Implement the generateUniqueId
function (a simple example is provided in the code above).
Now, you should be able to add new transactions using the form, and the application will reactively update the balance, income/expenses, and transaction list.
4.5 Implementing “Delete Transaction” Functionality in TransactionList.vue
and App.vue
To enable transaction deletion, we need to add a delete button to each transaction item in TransactionList.vue
and emit an event when it’s clicked. App.vue
will handle the event and update the transactions
array.
1. Add Delete Button and Click Event in TransactionList.vue
Template:
In TransactionList.vue
’s template, add a <button class="delete-btn">x</button>
within each <li>
item. Attach a @click
event listener to this button that calls a deleteTransaction
function and passes the transaction.id
.
<template>
<div>
<h3>History</h3>
<ul id="list" class="list">
<li v-for="transaction in props.transactions" :key="transaction.id" :class="{ minus: transaction.amount < 0, plus: transaction.amount > 0 }">
{{ transaction.text }} <span>{{ transaction.amount < 0 ? '-' : '+' }}${{ Math.abs(transaction.amount) }}</span><button class="delete-btn" @click="deleteTransaction(transaction.id)">x</button> <--- Delete button with click event
</li>
</ul>
</div>
</template>
2. Define deleteTransaction
Function and Emit transactionDeleted
Event in TransactionList.vue
Script:
In TransactionList.vue
’s <script setup>
section, define the deleteTransaction
function. This function should use defineEmits
to declare a custom event transactionDeleted
and emit this event with the transaction ID when the delete button is clicked.
<script setup>
import { defineProps, defineEmits } from 'vue';
import { useToast } from 'vue-toastify'; // Import useToast
const props = defineProps({
transactions: {
type: Array,
required: true
}
});
const emit = defineEmits(['transactionDeleted']); // Define custom event
const toast = useToast(); // Initialize toast
const deleteTransaction = (id) => {
emit('transactionDeleted', id); // Emit custom event with transaction ID
toast.success("Transaction deleted"); // Success toast
};
</script>
3. Handle transactionDeleted
Event in App.vue
:
In App.vue
’s template, listen for the transactionDeleted
custom event on the <TransactionList>
component. Create a handler function handleTransactionDeleted
in App.vue
’s script to receive the transaction ID and filter out the transaction with that ID from the transactions
array.
<template>
<div class="container">
<Header />
<Balance :total="total" />
<IncomeExpenses :income="income" :expenses="expenses" />
<TransactionList :transactions="transactions" @transactionDeleted="handleTransactionDeleted" /> <--- Listen for event
<AddTransaction @transactionSubmitted="handleTransactionSubmitted" />
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { useToast } from 'vue-toastify'; // Import useToast
// ... other imports
const transactions = ref([ /* ... initial transactions ... */ ]);
// ... total, income, expenses computed properties ...
const toast = useToast(); // Initialize toast
const handleTransactionSubmitted = (transactionData) => { /* ... existing function ... */ };
const handleTransactionDeleted = (id) => {
transactions.value = transactions.value.filter(transaction => transaction.id !== id); // Filter out deleted transaction
toast.success("Transaction deleted"); // Success toast
};
function generateUniqueId() { /* ... existing function ... */ }
</script>
Now, you should be able to delete transactions by clicking the “x” button next to each transaction item. The application will reactively update all balances and the transaction list.
5. Data Persistence with Local Storage
To make the expense tracker data persistent across browser sessions, we will implement local storage functionality.
Local Storage: A web storage API that allows web applications to store key-value pairs in a web browser with no expiration time. Data stored in local storage persists even after the browser is closed and reopened.
1. Load Transactions from Local Storage on Component Mount in App.vue
:
Use the onMounted
lifecycle hook in App.vue
to check if there are saved transactions in local storage when the component is mounted. If found, parse the JSON string from local storage and set it as the initial value for the transactions
ref.
<script setup>
import { ref, computed, onMounted } from 'vue'; // Import onMounted
import { useToast } from 'vue-toastify';
// ... other imports
const transactions = ref([]); // Initialize as empty array
onMounted(() => {
const savedTransactions = localStorage.getItem('transactions');
if (savedTransactions) {
transactions.value = JSON.parse(savedTransactions);
}
});
// ... other script code ...
</script>
2. Save Transactions to Local Storage Whenever Transactions Change in App.vue
:
Create a function saveTransactionsToLocalStorage
that stringifies the transactions.value
array and saves it to local storage with the key ‘transactions’. Call this function after adding a new transaction in handleTransactionSubmitted
and after deleting a transaction in handleTransactionDeleted
.
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useToast } from 'vue-toastify';
// ... other imports
const transactions = ref([]);
// ... onMounted hook ...
const handleTransactionSubmitted = (transactionData) => {
const newTransaction = { /* ... existing code ... */ };
transactions.value.push(newTransaction);
saveTransactionsToLocalStorage(); // Save to local storage
toast.success("Transaction added");
};
const handleTransactionDeleted = (id) => {
transactions.value = transactions.value.filter(transaction => transaction.id !== id);
saveTransactionsToLocalStorage(); // Save to local storage
toast.success("Transaction deleted");
};
function generateUniqueId() { /* ... existing function ... */ }
function saveTransactionsToLocalStorage() {
localStorage.setItem('transactions', JSON.stringify(transactions.value));
}
</script>
Now, your expense tracker application will persist data in local storage. Transactions will be saved when added or deleted, and they will be loaded from local storage when the application is loaded, even after closing and reopening the browser.
Conclusion
This chapter has provided a comprehensive guide to building an expense tracker application using Vue.js 3 and the Composition API. You have learned how to:
- Set up a Vue.js 3 project using Vue CLI.
- Structure your application using reusable components.
- Utilize the Composition API and
<script setup>
syntax for component logic. - Pass data between components using props.
- Implement reactivity using
ref
and computed properties. - Handle user input and form submissions.
- Emit and handle custom events between components.
- Use View Toastify for user notifications.
- Persist application data using local storage.
This project serves as a solid foundation for further exploration of Vue.js 3 and front-end development concepts. You can extend this application by adding features like editing transactions, filtering transactions by date, or implementing user authentication.