Build a Simple Next.js Interactive Personal Finance Tracker

Managing personal finances can feel like navigating a complex maze. Tracking income, expenses, and savings often involves spreadsheets, multiple apps, and a whole lot of manual data entry. Wouldn’t it be amazing to have a simple, intuitive tool that helps you visualize your financial health at a glance? In this article, we’ll build a Next.js interactive personal finance tracker, a project perfect for beginners and intermediate developers looking to hone their skills. We’ll break down the process step-by-step, explaining concepts in plain language and providing real-world examples. By the end, you’ll have a functional application and a solid understanding of Next.js fundamentals.

Why Build a Personal Finance Tracker?

Beyond the personal benefits of financial organization, building this project offers several advantages for developers:

  • Learn Core Next.js Concepts: You’ll gain hands-on experience with routing, state management, data fetching, and component design – essential skills for any Next.js developer.
  • Practical Application: Creating a real-world application reinforces your understanding of the framework and how it solves common web development challenges.
  • Portfolio Piece: A functional finance tracker is a great addition to your portfolio, showcasing your ability to build interactive and data-driven applications.
  • Problem-Solving: You’ll practice breaking down a complex problem (financial tracking) into manageable components and implementing solutions.

Let’s dive in and start building!

Project Setup: Setting the Stage

Before we start coding, let’s set up our development environment. We’ll use Node.js and npm (Node Package Manager) or yarn to manage our dependencies. Make sure you have Node.js installed on your system. If not, download and install it from the official Node.js website.

Open your terminal or command prompt and run the following command to create a new Next.js project:

npx create-next-app personal-finance-tracker

This command creates a new directory named `personal-finance-tracker` with the basic structure of a Next.js application. Navigate into this directory using:

cd personal-finance-tracker

Next, start the development server by running:

npm run dev

or

yarn dev

This will start the development server, usually on `http://localhost:3000`. Open this address in your browser to see the default Next.js welcome page. You’re now ready to start coding!

Project Structure and Core Components

Our finance tracker will consist of several key components:

  • Income Input: A form to add income entries, including the amount, source, and date.
  • Expense Input: A form to add expense entries, including the amount, category, and date.
  • Transactions List: A display of all income and expense transactions, with options for filtering and sorting.
  • Summary Dashboard: A visual overview of financial data, such as total income, total expenses, and balance.

We’ll organize our project with the following structure (you can modify this as needed):

personal-finance-tracker/
├── pages/
│   ├── index.js          # Main page (Dashboard)
│   └── transactions.js   # Transactions list page (optional)
├── components/
│   ├── IncomeForm.js
│   ├── ExpenseForm.js
│   ├── TransactionList.js
│   ├── SummaryDashboard.js
│   └── ... (Other components)
├── styles/
│   ├── globals.css     # Global styles
│   └── ... (Component-specific styles)
└── ... (Other files)

Building the Income and Expense Forms

Let’s create the forms for adding income and expenses. These forms will use HTML input elements and handle user input. We’ll use React’s state management to store the form data.

1. Create `IncomeForm.js` in the `components` directory:

import { useState } from 'react';

function IncomeForm({ onAddIncome }) {
  const [amount, setAmount] = useState('');
  const [source, setSource] = useState('');
  const [date, setDate] = useState('');

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

    const income = {
      amount: parseFloat(amount),
      source,
      date,
      type: 'income', // Add a 'type' property
    };
    onAddIncome(income);
    setAmount('');
    setSource('');
    setDate('');
  };

  return (
    <form onSubmit={handleSubmit} className="income-form">
      <h3>Add Income</h3>
      <div>
        <label htmlFor="amount">Amount:</label>
        <input
          type="number"
          id="amount"
          value={amount}
          onChange={(e) => setAmount(e.target.value)}
        />
      </div>
      <div>
        <label htmlFor="source">Source:</label>
        <input
          type="text"
          id="source"
          value={source}
          onChange={(e) => setSource(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 Income</button>
      <style jsx>{`
        .income-form {
          border: 1px solid #ccc;
          padding: 1rem;
          margin-bottom: 1rem;
        }
        label {
          display: block;
          margin-bottom: 0.5rem;
        }
        input {
          width: 100%;
          padding: 0.5rem;
          margin-bottom: 1rem;
        }
        button {
          background-color: #4CAF50;
          color: white;
          padding: 0.75rem 1rem;
          border: none;
          cursor: pointer;
        }
      `}</style>
    </form>
  );
}

export default IncomeForm;

2. Create `ExpenseForm.js` in the `components` directory:

import { useState } from 'react';

function ExpenseForm({ onAddExpense }) {
  const [amount, setAmount] = useState('');
  const [category, setCategory] = useState('');
  const [date, setDate] = useState('');

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

    const expense = {
      amount: parseFloat(amount),
      category,
      date,
      type: 'expense', // Add a 'type' property
    };
    onAddExpense(expense);
    setAmount('');
    setCategory('');
    setDate('');
  };

  return (
    <form onSubmit={handleSubmit} className="expense-form">
      <h3>Add Expense</h3>
      <div>
        <label htmlFor="amount">Amount:</label>
        <input
          type="number"
          id="amount"
          value={amount}
          onChange={(e) => setAmount(e.target.value)}
        />
      </div>
      <div>
        <label htmlFor="category">Category:</label>
        <input
          type="text"
          id="category"
          value={category}
          onChange={(e) => setCategory(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>
      <style jsx>{`
        .expense-form {
          border: 1px solid #ccc;
          padding: 1rem;
          margin-bottom: 1rem;
        }
        label {
          display: block;
          margin-bottom: 0.5rem;
        }
        input {
          width: 100%;
          padding: 0.5rem;
          margin-bottom: 1rem;
        }
        button {
          background-color: #f44336;
          color: white;
          padding: 0.75rem 1rem;
          border: none;
          cursor: pointer;
        }
      `}</style>
    </form>
  );
}

export default ExpenseForm;

Key points in these forms:

  • useState Hook: We use the `useState` hook to manage the form input values. Each input field has its own state variable.
  • handleSubmit Function: This function is called when the form is submitted. It prevents the default form submission behavior, validates the input, creates an income or expense object, and calls the `onAddIncome` or `onAddExpense` function (passed as a prop) to add the transaction. It also resets the form fields after submission.
  • onAddIncome/onAddExpense Props: These props are functions that will be passed down from the parent component (e.g., `index.js`) to handle adding the transaction to the overall list.
  • Inline Styling: We use a `<style jsx>` block for simple component-specific styling. You can use a separate CSS file or a CSS-in-JS solution (like styled-components) for more complex styling.

Creating the Transaction List Component

This component will display the list of income and expense transactions. It will receive the transaction data as a prop and render each transaction in a user-friendly format.

Create `TransactionList.js` in the `components` directory:

function TransactionList({ transactions }) {
  return (
    <div className="transaction-list">
      <h3>Transactions</h3>
      <ul>
        {transactions.map((transaction, index) => (
          <li key={index} className={transaction.type === 'income' ? 'income-item' : 'expense-item'}>
            <span>{transaction.date}: </span>
            <span>{transaction.type === 'income' ? transaction.source : transaction.category}: </span>
            <span>${transaction.amount.toFixed(2)}</span>
          </li>
        ))}
      </ul>
      <style jsx>{`
        .transaction-list {
          border: 1px solid #ccc;
          padding: 1rem;
        }
        ul {
          list-style: none;
          padding: 0;
        }
        li {
          padding: 0.5rem 0;
          border-bottom: 1px solid #eee;
        }
        .income-item {
          color: green;
        }
        .expense-item {
          color: red;
        }
      `}</style>
    </div>
  );
}

export default TransactionList;

Key points:

  • Transactions Prop: This component receives an array of `transactions` as a prop.
  • .map() Function: We use the `map()` function to iterate over the `transactions` array and render a `<li>` element for each transaction.
  • Conditional Styling: We use conditional styling (e.g., `className={transaction.type === ‘income’ ? ‘income-item’ : ‘expense-item’}`) to apply different styles to income and expense items.
  • .toFixed(2): The `.toFixed(2)` method formats the amount to two decimal places for currency display.

Building the Summary Dashboard

The summary dashboard will provide a quick overview of the user’s financial status. It will calculate and display total income, total expenses, and the net balance.

Create `SummaryDashboard.js` in the `components` directory:

function SummaryDashboard({ transactions }) {
  // Calculate totals
  const totalIncome = transactions
    .filter((transaction) => transaction.type === 'income')
    .reduce((sum, transaction) => sum + transaction.amount, 0);

  const totalExpenses = transactions
    .filter((transaction) => transaction.type === 'expense')
    .reduce((sum, transaction) => sum + transaction.amount, 0);

  const balance = totalIncome - totalExpenses;

  return (
    <div className="summary-dashboard">
      <h3>Summary</h3>
      <div className="summary-item">
        <span>Total Income:</span> <span>${totalIncome.toFixed(2)}</span>
      </div>
      <div className="summary-item">
        <span>Total Expenses:</span> <span>${totalExpenses.toFixed(2)}</span>
      </div>
      <div className="summary-item balance">
        <span>Balance:</span> <span>${balance.toFixed(2)}</span>
      </div>
      <style jsx>{`
        .summary-dashboard {
          border: 1px solid #ccc;
          padding: 1rem;
          margin-bottom: 1rem;
        }
        .summary-item {
          display: flex;
          justify-content: space-between;
          padding: 0.5rem 0;
        }
        .balance {
          font-weight: bold;
        }
      `}</style>
    </div>
  );
}

export default SummaryDashboard;

Key points:

  • Filtering Transactions: We use the `filter()` method to separate income and expense transactions.
  • Reducing Values: We use the `reduce()` method to calculate the sum of the amounts for both income and expenses.
  • Balance Calculation: The balance is calculated by subtracting total expenses from total income.

Putting it All Together: The Main Page (index.js)

Now, let’s integrate all the components into the main page (`pages/index.js`). This page will hold the income and expense forms, the transaction list, and the summary dashboard.

Modify `pages/index.js`:

import { useState } from 'react';
import IncomeForm from '../components/IncomeForm';
import ExpenseForm from '../components/ExpenseForm';
import TransactionList from '../components/TransactionList';
import SummaryDashboard from '../components/SummaryDashboard';

function HomePage() {
  const [transactions, setTransactions] = useState([]);

  const handleAddIncome = (income) => {
    setTransactions((prevTransactions) => [...prevTransactions, income]);
  };

  const handleAddExpense = (expense) => {
    setTransactions((prevTransactions) => [...prevTransactions, expense]);
  };

  return (
    <div className="container">
      <h1>Personal Finance Tracker</h1>
      <SummaryDashboard transactions={transactions} />
      <div className="forms-container">
        <IncomeForm onAddIncome={handleAddIncome} />
        <ExpenseForm onAddExpense={handleAddExpense} />
      </div>
      <TransactionList transactions={transactions} />
      <style jsx>{`
        .container {
          padding: 2rem;
          max-width: 800px;
          margin: 0 auto;
        }
        .forms-container {
          display: flex;
          gap: 1rem;
          margin-bottom: 1rem;
        }
      `}</style>
    </div>
  );
}

export default HomePage;

Key points:

  • Import Components: We import the `IncomeForm`, `ExpenseForm`, `TransactionList`, and `SummaryDashboard` components.
  • useState for Transactions: We use the `useState` hook to manage the `transactions` array, which holds all income and expense data. This is the single source of truth for our transaction data.
  • handleAddIncome and handleAddExpense Functions: These functions are called when the respective forms submit data. They update the `transactions` state by adding the new income or expense item. The spread operator (`…prevTransactions`) is used to create a new array, ensuring that the state is updated correctly.
  • Passing Props: We pass the `transactions` array to the `TransactionList` and `SummaryDashboard` components, and the `handleAddIncome` and `handleAddExpense` functions to the `IncomeForm` and `ExpenseForm` components.

Running and Testing Your Application

With all the components in place, it’s time to test your application. Save all your files, and if your development server isn’t already running, start it using `npm run dev` or `yarn dev`. Open your browser to `http://localhost:3000`.

You should see the main page with the income and expense forms and the transaction list. Try entering some income and expense data. Verify that:

  • The transactions are added to the list.
  • The summary dashboard updates correctly.
  • The styling is applied as expected.

If you encounter any issues, check the browser’s developer console for error messages. Double-check your code against the examples provided and ensure that all components are imported and used correctly.

Adding Features: Enhancements and Next Steps

This is a basic finance tracker, but you can extend it with several features:

  • Data Persistence: Instead of storing data in the component’s state (which is lost on refresh), implement data persistence using local storage, cookies, or a database (like Firebase or Supabase).
  • Categorization: Add categories to expenses (e.g., food, transportation, housing) to analyze spending patterns.
  • Filtering and Sorting: Implement filtering and sorting options for the transaction list (e.g., by date, category, or amount).
  • Budgeting: Add a budgeting feature to set monthly budgets for different categories.
  • Charts and Graphs: Use libraries like Chart.js or Recharts to visualize financial data with charts and graphs.
  • User Authentication: Implement user authentication to allow multiple users to track their finances securely.
  • Responsive Design: Make the application responsive so it works well on different screen sizes (phones, tablets, desktops).

Common Mistakes and How to Fix Them

Here are some common mistakes beginners make when building Next.js applications, and how to avoid them:

  • Incorrect Imports: Double-check that you’re importing components and modules correctly. Pay attention to file paths and capitalization.
  • State Management Issues: Make sure you’re updating state correctly using the `useState` hook and that you understand how state changes trigger re-renders. Avoid directly modifying state objects; always create new ones.
  • Prop Drilling: Avoid passing props through multiple levels of components unnecessarily. Consider using Context API or a state management library (like Redux or Zustand) for more complex state management.
  • CSS Styling Problems: Make sure you understand how CSS modules, styled-components, or other styling solutions work in Next.js. Check your browser’s developer tools to see if styles are being applied correctly.
  • Asynchronous Operations: When fetching data from an API or performing other asynchronous operations, make sure you handle loading states and error conditions appropriately. Use `useEffect` hook to handle side effects.

Key Takeaways and Summary

In this article, we’ve built a simple yet functional personal finance tracker using Next.js. We covered:

  • Setting up a Next.js project.
  • Creating reusable components for forms, transaction lists, and dashboards.
  • Managing state with the `useState` hook.
  • Passing data between components using props.
  • Basic styling using inline styles and the `<style jsx>` tag.

This project provides a solid foundation for building more complex web applications with Next.js. Remember to experiment, try new features, and consult the Next.js documentation when you get stuck. The more you practice, the more comfortable you’ll become with this powerful framework. With each project, you will not only improve your coding skills but also gain a deeper understanding of web development principles. Embrace the learning process, and enjoy building!