Next.js: Build a Simple Interactive Expense Tracker

Written by

in

Managing finances can be a daunting task. Keeping track of income and expenses, categorizing transactions, and understanding where your money goes often feels overwhelming. Wouldn’t it be great to have a simple, user-friendly tool to visualize your spending habits and gain better control of your finances? In this tutorial, we’ll build a basic interactive expense tracker using Next.js, empowering you to easily log your transactions, see where your money is going, and make informed financial decisions. This project is perfect for beginners and intermediate developers looking to enhance their Next.js skills while creating something practical and useful.

Why Build an Expense Tracker?

An expense tracker isn’t just about numbers; it’s about awareness. It helps you:

  • Understand Your Spending: Identify where your money is actually going.
  • Set Budgets: Create realistic budgets and stick to them.
  • Identify Savings Opportunities: Spot areas where you can cut back on spending.
  • Achieve Financial Goals: Track progress towards your financial objectives.

Building one yourself gives you complete control over the features and functionality. You can tailor it precisely to your needs, learn new technologies (like Next.js), and gain valuable experience in web development.

Project Overview

Our expense tracker will be a single-page application (SPA) built with Next.js. It will allow users to:

  • Add new expenses with a description, amount, and category.
  • View a list of expenses.
  • Filter expenses by category.
  • See a summary of total expenses.

We’ll keep it simple to focus on core concepts, but you can easily extend it with more features later.

Prerequisites

Before we dive in, make sure you have the following:

  • Node.js and npm (or yarn): You’ll need these to manage project dependencies and run the development server.
  • Basic understanding of JavaScript and React: Familiarity with these technologies will make it easier to follow along.
  • A code editor: VS Code, Sublime Text, or any editor of your choice.

Step-by-Step Guide

1. Setting Up the Project

First, let’s create a new Next.js project. Open your terminal and run the following command:

npx create-next-app expense-tracker

This command will create a new directory called expense-tracker with all the necessary files. Navigate into the project directory:

cd expense-tracker

Now, start the development server:

npm run dev

This will start the development server, usually on http://localhost:3000. Open this address in your browser, and you should see the default Next.js welcome page.

2. Project Structure

Next.js projects have a specific file structure. Here’s how we’ll organize our project:

  • pages/: This directory contains your application’s pages. Each file in this directory represents a route. We’ll have a index.js file which will be our main page.
  • components/: This directory will hold our reusable React components.
  • styles/: This directory will hold our CSS styles.
  • public/: This directory will contain static assets like images.

3. Creating the Expense Input Form

Let’s create the form to add new expenses. Inside the components directory, create a file called ExpenseForm.js. Add the following code:

// components/ExpenseForm.js
import { useState } from 'react';

const ExpenseForm = ({ onAddExpense }) => {
  const [description, setDescription] = useState('');
  const [amount, setAmount] = useState('');
  const [category, setCategory] = useState('food'); // Default category

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!description || !amount) {
      alert('Please fill in all fields.');
      return;
    }

    const newExpense = {
      description,
      amount: parseFloat(amount),
      category,
      date: new Date().toLocaleDateString(), // Add a date
    };

    onAddExpense(newExpense);
    setDescription('');
    setAmount('');
    setCategory('food');
  };

  return (
    <form onSubmit={handleSubmit} className="expense-form">
      <h3>Add Expense</h3>
      <div className="form-group">
        <label htmlFor="description">Description:</label>
        <input
          type="text"
          id="description"
          value={description}
          onChange={(e) => setDescription(e.target.value)}
        />
      </div>
      <div className="form-group">
        <label htmlFor="amount">Amount:</label>
        <input
          type="number"
          id="amount"
          value={amount}
          onChange={(e) => setAmount(e.target.value)}
        />
      </div>
      <div className="form-group">
        <label htmlFor="category">Category:</label>
        <select
          id="category"
          value={category}
          onChange={(e) => setCategory(e.target.value)}
        >
          <option value="food">Food</option>
          <option value="transportation">Transportation</option>
          <option value="housing">Housing</option>
          <option value="entertainment">Entertainment</option>
          <option value="other">Other</option>
        </select>
      </div>
      <button type="submit">Add Expense</button>
    </form>
  );
};

export default ExpenseForm;

This component uses the useState hook to manage the form input values. It also includes a handleSubmit function that is called when the form is submitted. The function prevents the default form submission behavior, validates the input, creates a new expense object, and calls the onAddExpense function (which we’ll define later to pass data up to the parent component). Don’t forget to add basic styling, we’ll do that later.

4. Creating the Expense List Component

Now, let’s create a component to display the list of expenses. Create a file named ExpenseList.js inside the components directory. Add the following code:


// components/ExpenseList.js
const ExpenseList = ({ expenses, onCategoryFilter }) => {
  const categories = [...new Set(expenses.map(expense => expense.category))];

  return (
    <div>
      <h3>Expenses</h3>
      <div className="category-filters">
        <button onClick={() => onCategoryFilter('all')} className="category-button">All</button>
        {categories.map(category => (
          <button key={category} onClick={() => onCategoryFilter(category)} className="category-button">
            {category.charAt(0).toUpperCase() + category.slice(1)}
          </button>
        ))}
      </div>
      <ul className="expense-list">
        {expenses.map((expense) => (
          <li key={expense.description} className="expense-item">
            <span>{expense.description}</span>
            <span>${expense.amount.toFixed(2)}</span>
            <span>({expense.category})</span>
            <span>{expense.date}</span>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default ExpenseList;

This component takes an array of expenses as a prop and displays them in a list. It also includes the filters by category. We’ll pass the expenses data and the filtering function from the parent component.

5. Integrating Components in the Main Page

Now, let’s integrate these components into our main page (pages/index.js). Replace the content of pages/index.js with the following code:


// pages/index.js
import { useState } from 'react';
import ExpenseForm from '../components/ExpenseForm';
import ExpenseList from '../components/ExpenseList';

export default function Home() {
  const [expenses, setExpenses] = useState([]);
  const [categoryFilter, setCategoryFilter] = useState('all');

  const handleAddExpense = (newExpense) => {
    setExpenses([...expenses, newExpense]);
  };

  const handleCategoryFilter = (category) => {
    setCategoryFilter(category);
  };

  const filteredExpenses = categoryFilter === 'all' ? expenses : expenses.filter(expense => expense.category === categoryFilter);

  const totalExpenses = filteredExpenses.reduce((sum, expense) => sum + expense.amount, 0);

  return (
    <div className="container">
      <h1>Expense Tracker</h1>
      <ExpenseForm onAddExpense={handleAddExpense} />
      <div className="expense-summary">
        <h2>Total Expenses: ${totalExpenses.toFixed(2)}</h2>
      </div>
      <ExpenseList expenses={filteredExpenses} onCategoryFilter={handleCategoryFilter} />
    </div>
  );
}

Here, we import the ExpenseForm and ExpenseList components. We use the useState hook to manage the expenses state. The handleAddExpense function updates the expenses array when a new expense is added, and handleCategoryFilter updates the category filter. We then pass the expenses and the handleAddExpense function as props to the ExpenseForm component and the filtered expenses and handleCategoryFilter as props to the ExpenseList component.

6. Adding Basic Styling

Let’s add some basic styling to make the app look better. Create a file named styles/Home.module.css and add the following code:


/* styles/Home.module.css */
.container {
  max-width: 800px;
  margin: 20px auto;
  padding: 20px;
  border: 1px solid #ccc;
  border-radius: 8px;
  background-color: #f9f9f9;
}

h1 {
  text-align: center;
  color: #333;
}

.expense-form {
  margin-bottom: 20px;
  padding: 15px;
  border: 1px solid #ddd;
  border-radius: 5px;
  background-color: #fff;
}

.form-group {
  margin-bottom: 15px;
}

label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
}

input[type="text"], input[type="number"], select {
  width: 100%;
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
  box-sizing: border-box;
  margin-bottom: 10px;
}

button {
  background-color: #4CAF50;
  color: white;
  padding: 10px 15px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

button:hover {
  background-color: #3e8e41;
}

.expense-summary {
  text-align: center;
  margin-bottom: 20px;
  padding: 10px;
  background-color: #e9ecef;
  border-radius: 4px;
}

.expense-list {
  list-style: none;
  padding: 0;
}

.expense-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px;
  border-bottom: 1px solid #eee;
}

.expense-item:last-child {
  border-bottom: none;
}

.category-filters {
  margin-bottom: 10px;
  display: flex;
  gap: 10px;
}

.category-button {
  background-color: #007bff;
  color: white;
  padding: 8px 12px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.category-button:hover {
  background-color: #0056b3;
}

Import this CSS file into pages/index.js. Add the following line at the top of the file:

import styles from '../styles/Home.module.css';

And apply the styles to the corresponding elements. For example, add className={styles.container} to the main div in pages/index.js.

You may also add the styles to the child components. For example, add className={styles.expenseForm} to the form in ExpenseForm.js.

7. Testing and Enhancements

Now, test your application by adding some expenses. You should see them listed on the page. You can try the filtering feature too. Here are some ideas for enhancements:

  • Data Persistence: Store expenses in local storage, cookies, or a database (like Firebase or Supabase) so the data persists even when the browser is closed.
  • More Categories: Add more expense categories.
  • Date Picker: Allow users to select the date of the expense.
  • Expense Editing and Deletion: Add functionality to edit and delete expenses.
  • Charts and Graphs: Visualize the expenses using charts (e.g., with Chart.js).
  • Responsive Design: Make the app responsive so it looks good on different screen sizes.

8. Common Mistakes and Troubleshooting

Here are some common mistakes and how to fix them:

  • Incorrect import paths: Double-check that your import paths (e.g., ../components/ExpenseForm) are correct.
  • Typos in component names or prop names: Typos can lead to components not rendering correctly or data not being passed correctly.
  • Incorrect data types: Make sure you are parsing the amount to a number using parseFloat() before storing it in the state.
  • Missing or incorrect CSS classes: Ensure that you have applied the correct CSS classes to your elements and that your CSS is correctly linked.
  • State not updating: Make sure you are correctly updating the state using the setExpenses function.

9. Deploying Your App

Once you’re happy with your expense tracker, you can deploy it to the web. Here’s how to deploy it using Vercel (which is recommended for Next.js apps):

  1. Push your code to a Git repository: (GitHub, GitLab, or Bitbucket).
  2. Sign up for a Vercel account: Go to Vercel and sign up.
  3. Import your Git repository: In your Vercel dashboard, click “Import Project” and connect your Git repository.
  4. Deploy: Vercel will automatically detect that it’s a Next.js project and deploy it.

Vercel will provide you with a live URL where your expense tracker will be accessible.

Key Takeaways

  • You’ve learned how to create a basic expense tracker using Next.js.
  • You’ve gained experience with React components, state management, and event handling.
  • You understand how to structure a Next.js project.
  • You’ve seen how to add basic styling and deploy your app.

FAQ

  1. Can I use a different CSS framework instead of writing my own CSS? Yes, you can. Popular choices include Tailwind CSS, Bootstrap, and Material UI.
  2. How can I add data persistence? You can use local storage, cookies, or a database (like Firebase, Supabase, or MongoDB).
  3. How do I add authentication? You can integrate third-party authentication services like Auth0 or Firebase Authentication, or build your own authentication system.
  4. Can I use this app on my phone? Yes, it should work on your phone, but you might want to make it responsive for a better user experience.
  5. What are some good resources for learning more about Next.js? The official Next.js documentation is excellent. Also, check out online courses on platforms like Udemy, Coursera, and freeCodeCamp.

Building this expense tracker has provided a solid foundation in Next.js development. From here, you can continue to expand its features, add more advanced functionalities, and refine your skills. The journey of building applications is an ongoing process of learning, experimentation, and improvement. Embrace the challenges, celebrate your successes, and keep coding – the possibilities are endless!