Tired of losing track of your spending? In today’s digital age, managing your finances effectively is more crucial than ever. Keeping tabs on where your money goes can be a real headache, especially if you’re juggling multiple expenses and income streams. A simple, yet effective, expense tracker can make a world of difference. This tutorial will guide you through building an interactive web-based expense tracker using Next.js, empowering you to take control of your finances with a user-friendly and efficient tool. We’ll cover everything from setting up the project to implementing key features like adding expenses, displaying them, and calculating totals.
Why Build an Expense Tracker?
An expense tracker isn’t just about recording numbers; it’s about gaining insights into your spending habits. By visualizing where your money goes, you can identify areas where you might be overspending and make informed decisions to improve your financial health. This project will not only teach you Next.js but also provide you with a practical tool that you can use daily. Building this tracker will give you a solid foundation in React and Next.js, including how to handle user input, manage state, and build a basic user interface.
What We’ll Build
Our expense tracker will allow users to:
- Add new expenses with a description, amount, and date.
- View a list of all expenses.
- See a running total of expenses.
- (Optional) Filter expenses by date range.
This project is designed to be beginner-friendly. We’ll break down each step, explaining the concepts and code in detail. We’ll also cover common pitfalls and provide solutions, ensuring you have a smooth learning experience.
Prerequisites
Before we dive in, make sure you have the following:
- Node.js and npm (or yarn) installed on your system.
- A basic understanding of HTML, CSS, and JavaScript.
- A code editor (like VS Code, Sublime Text, or Atom).
Setting Up Your Next.js Project
Let’s get started by creating a new Next.js project. Open your terminal and run the following command:
npx create-next-app expense-tracker
This command will set up a new Next.js project named expense-tracker. Navigate into your project directory:
cd expense-tracker
Now, start the development server:
npm run dev
Open your browser and go to http://localhost:3000. You should see the default Next.js welcome page.
Project Structure
Before we start coding, let’s briefly look at the project structure:
pages/: This directory contains your pages. Each file in this directory becomes a route. For example,pages/index.jswill be accessible at the root path (/).components/: This directory will hold your reusable React components.styles/: This directory is where you’ll put your CSS or styling files.public/: This directory is for static assets like images and fonts.
Creating the Expense Input Form
Let’s create a form where users can input their expenses. We’ll create a new component for this. Inside the components directory, create a file named ExpenseForm.js.
Here’s the code for ExpenseForm.js:
import { useState } from 'react';
function ExpenseForm({ onAddExpense }) {
const [description, setDescription] = useState('');
const [amount, setAmount] = useState('');
const [date, setDate] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
// Basic validation
if (!description || !amount || !date) {
alert('Please fill in all fields.');
return;
}
const newExpense = {
description: description,
amount: parseFloat(amount),
date: date,
};
onAddExpense(newExpense);
// Clear the form
setDescription('');
setAmount('');
setDate('');
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="description">Description:</label>
<input
type="text"
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</div>
<div>
<label htmlFor="amount">Amount:</label>
<input
type="number"
id="amount"
value={amount}
onChange={(e) => setAmount(e.target.value)}
/>
</div>
<div>
<label htmlFor="date">Date:</label>
<input
type="date"
id="date"
value={date}
onChange={(e) => setDate(e.target.value)}
/>
</div>
<button type="submit">Add Expense</button>
</form>
);
}
export default ExpenseForm;
Let’s break down the code:
- We import
useStatefrom React to manage the form input values. - We define state variables for
description,amount, anddate. - The
handleSubmitfunction is called when the form is submitted. It prevents the default form submission behavior, validates the input, creates a new expense object, calls theonAddExpensefunction (which we’ll define later), and clears the form. - The form includes input fields for description, amount, and date, each bound to its corresponding state variable. The
onChangeevent updates the state when the user types in the input fields. - The
onAddExpenseprop is passed to this component from the parent component (index.js).
Displaying the Expense Form in the Main Page
Now, let’s include the ExpenseForm component in our main page (pages/index.js). Open pages/index.js and modify it as follows:
import { useState } from 'react';
import ExpenseForm from '../components/ExpenseForm';
function HomePage() {
const [expenses, setExpenses] = useState([]);
const handleAddExpense = (newExpense) => {
setExpenses([...expenses, newExpense]);
};
return (
<div>
<h2>Expense Tracker</h2>
<ExpenseForm onAddExpense={handleAddExpense} />
<h3>Expenses</h3>
<ul>
{expenses.map((expense, index) => (
<li key={index}>
{expense.description} - ${expense.amount} - {expense.date}
</li>
))}
</ul>
</div>
);
}
export default HomePage;
Here’s what’s happening:
- We import the
ExpenseFormcomponent. - We define a state variable
expenses, which is an array to hold the expense objects. - The
handleAddExpensefunction updates theexpensesstate by adding a new expense. It uses the spread operator (...) to add the new expense to the existing array. - We render the
ExpenseFormcomponent and pass thehandleAddExpensefunction as a prop calledonAddExpense. This allows theExpenseFormto communicate with the parent component and update the expenses list. - We display the list of expenses using the
mapfunction to iterate through theexpensesarray.
Now, try adding an expense using the form. You should see it appear in the list below the form.
Styling the Expense Tracker
Let’s add some basic styling to make our expense tracker more visually appealing. Create a file named styles/ExpenseTracker.module.css and add the following CSS rules:
.container {
max-width: 800px;
margin: 20px auto;
padding: 20px;
border: 1px solid #ccc;
border-radius: 5px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="text"], input[type="number"], input[type="date"] {
width: 100%;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
button {
background-color: #4CAF50;
color: white;
padding: 12px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
ul {
list-style: none;
padding: 0;
}
li {
padding: 10px;
border-bottom: 1px solid #eee;
}
Next, import this CSS module into your pages/index.js file. Modify pages/index.js to include the styling:
import { useState } from 'react';
import ExpenseForm from '../components/ExpenseForm';
import styles from '../styles/ExpenseTracker.module.css';
function HomePage() {
const [expenses, setExpenses] = useState([]);
const handleAddExpense = (newExpense) => {
setExpenses([...expenses, newExpense]);
};
return (
<div className={styles.container}>
<h2>Expense Tracker</h2>
<ExpenseForm onAddExpense={handleAddExpense} />
<h3>Expenses</h3>
<ul>
{expenses.map((expense, index) => (
<li key={index}>
{expense.description} - ${expense.amount} - {expense.date}
</li>
))}
</ul>
</div>
);
}
export default HomePage;
We import the styles using import styles from '../styles/ExpenseTracker.module.css'; and then apply the class names to the relevant elements using className={styles.container}. This is how you use CSS modules in Next.js.
Calculating the Total Expenses
Let’s add a feature to calculate and display the total expenses. Modify your pages/index.js to include the following:
import { useState, useMemo } from 'react';
import ExpenseForm from '../components/ExpenseForm';
import styles from '../styles/ExpenseTracker.module.css';
function HomePage() {
const [expenses, setExpenses] = useState([]);
const handleAddExpense = (newExpense) => {
setExpenses([...expenses, newExpense]);
};
const totalExpenses = useMemo(() => {
return expenses.reduce((sum, expense) => sum + expense.amount, 0);
}, [expenses]);
return (
<div className={styles.container}>
<h2>Expense Tracker</h2>
<ExpenseForm onAddExpense={handleAddExpense} />
<h3>Expenses</h3>
<p>Total: ${totalExpenses.toFixed(2)}</p>
<ul>
{expenses.map((expense, index) => (
<li key={index}>
{expense.description} - ${expense.amount} - {expense.date}
</li>
))}
</ul>
</div>
);
}
export default HomePage;
Here’s what’s new:
- We import
useMemofrom React. - We use
useMemoto calculate thetotalExpenses. This memoizes the calculation, so it only re-runs when theexpensesarray changes, optimizing performance. - Inside
useMemo, we use thereducemethod to sum theamountproperty of each expense object. - We display the
totalExpensesbelow the expenses list, formatted to two decimal places usingtoFixed(2).
Adding Date Filtering (Optional)
Let’s implement an optional feature to filter expenses by date range. This will involve adding date input fields and a filter function.
Modify your pages/index.js as follows:
import { useState, useMemo } from 'react';
import ExpenseForm from '../components/ExpenseForm';
import styles from '../styles/ExpenseTracker.module.css';
function HomePage() {
const [expenses, setExpenses] = useState([]);
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const handleAddExpense = (newExpense) => {
setExpenses([...expenses, newExpense]);
};
const filteredExpenses = useMemo(() => {
let filtered = expenses;
if (startDate) {
filtered = filtered.filter(expense => expense.date >= startDate);
}
if (endDate) {
filtered = filtered.filter(expense => expense.date <= endDate);
}
return filtered;
}, [expenses, startDate, endDate]);
const totalExpenses = useMemo(() => {
return filteredExpenses.reduce((sum, expense) => sum + expense.amount, 0);
}, [filteredExpenses]);
return (
<div className={styles.container}>
<h2>Expense Tracker</h2>
<ExpenseForm onAddExpense={handleAddExpense} />
<div className={styles['form-group']}>
<label htmlFor="startDate">Start Date:</label>
<input
type="date"
id="startDate"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
/>
</div>
<div className={styles['form-group']}>
<label htmlFor="endDate">End Date:</label>
<input
type="date"
id="endDate"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
/>
</div>
<h3>Expenses</h3>
<p>Total: ${totalExpenses.toFixed(2)}</p>
<ul>
{filteredExpenses.map((expense, index) => (
<li key={index}>
{expense.description} - ${expense.amount} - {expense.date}
</li>
))}
</ul>
</div>
);
}
export default HomePage;
Here’s what changed:
- We added state variables for
startDateandendDateto store the filter dates. - We created a
filteredExpensesvariable usinguseMemoto filter the expenses based on the start and end dates. - The
totalExpensesis now calculated based on thefilteredExpensesarray. - We added date input fields for start and end dates.
- We updated the list rendering to use
filteredExpenses.
Remember to add the form-group class in the CSS module for the date input fields.
Handling Common Mistakes
Here are some common mistakes and how to fix them:
- Incorrect imports: Double-check that you’re importing the necessary components and functions correctly. For example, ensure you’re importing
useStatefromreactand not from any other library. - State updates not working: When updating state using
useState, always use the setter function (e.g.,setExpenses) to update the state. Avoid directly modifying the state variable. - Missing dependencies in
useMemo: Make sure you include all dependencies in the dependency array ofuseMemoanduseEffecthooks. Otherwise, the calculation or effect might not update when the relevant values change. - Incorrect data types: Ensure that the data types in your application are correct. For example, use
parseFloat()to convert the amount from a string to a number. - CSS Module Issues: Ensure your CSS file is correctly imported and that you are using the correct class names.
Key Takeaways
- We built a functional expense tracker using Next.js.
- We learned how to handle user input, manage state, and display data.
- We implemented features such as adding expenses, calculating totals, and filtering by date.
- We used CSS modules for styling.
FAQ
Q: How can I store the expense data permanently?
A: Currently, the expense data is stored in the component’s state, which means it’s lost when the page is refreshed. To persist the data, you can use local storage, session storage, or a database. For local storage, you can use the localStorage API to save the expenses as a JSON string and retrieve them when the component mounts. For more advanced features, consider using a database or server-side rendering with Next.js.
Q: How can I add more features?
A: You can extend this project by adding features such as:
- Editing and deleting expenses.
- Categorizing expenses.
- Generating reports and charts.
- User authentication.
Q: Why use Next.js for this project?
A: Next.js provides several benefits for this project, including:
- Server-side rendering (SSR) and static site generation (SSG) for improved SEO and performance.
- Built-in routing and API routes.
- Easy integration with other libraries and frameworks.
- Developer-friendly features like hot module replacement and fast refresh.
Q: How can I deploy this app?
A: You can deploy your Next.js app to platforms like Vercel (which is built by the creators of Next.js), Netlify, or other hosting providers. Vercel provides a seamless deployment experience for Next.js apps.
Conclusion
By following this tutorial, you’ve successfully built a basic expense tracker using Next.js. You’ve learned about essential concepts like state management, component creation, and styling. This project provides a solid foundation for building more complex web applications. Embrace the power of Next.js, and keep experimenting with new features and functionalities to create even more sophisticated tools. Remember, the journey of a thousand lines of code begins with a single step. Continue to learn, build, and refine your skills, and you’ll be amazed at what you can create.
