Build a Simple Next.js Interactive Web-Based Expense Tracker

Written by

in

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.js will 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 useState from React to manage the form input values.
  • We define state variables for description, amount, and date.
  • The handleSubmit function is called when the form is submitted. It prevents the default form submission behavior, validates the input, creates a new expense object, calls the onAddExpense function (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 onChange event updates the state when the user types in the input fields.
  • The onAddExpense prop 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 ExpenseForm component.
  • We define a state variable expenses, which is an array to hold the expense objects.
  • The handleAddExpense function updates the expenses state by adding a new expense. It uses the spread operator (...) to add the new expense to the existing array.
  • We render the ExpenseForm component and pass the handleAddExpense function as a prop called onAddExpense. This allows the ExpenseForm to communicate with the parent component and update the expenses list.
  • We display the list of expenses using the map function to iterate through the expenses array.

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 useMemo from React.
  • We use useMemo to calculate the totalExpenses. This memoizes the calculation, so it only re-runs when the expenses array changes, optimizing performance.
  • Inside useMemo, we use the reduce method to sum the amount property of each expense object.
  • We display the totalExpenses below the expenses list, formatted to two decimal places using toFixed(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 startDate and endDate to store the filter dates.
  • We created a filteredExpenses variable using useMemo to filter the expenses based on the start and end dates.
  • The totalExpenses is now calculated based on the filteredExpenses array.
  • 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 useState from react and 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 of useMemo and useEffect hooks. 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.