React JS tutorial: Building an expense tracker with local storage

1. Introduction

Welcome to our hands-on React JS tutorial! In this comprehensive guide, we’ll walk you through building a fully functional expense tracker using React, enriched with local storage for persistent data storage.

Managing expenses is a common requirement for many web applications, and integrating React with local storage provides a user-friendly solution that retains data even after a page refresh. Whether you’re a React beginner looking to enhance your skills or an experienced developer exploring local storage capabilities, this React JS tutorial is designed to cater to all levels.

Let’s dive into creating a powerful yet straightforward expense tracker with the flexibility of React and the reliability of local storage.

Throughout this React JS tutorial, you can refer to the entire code for assistance and explore a live demo through the provided links below.

2. Prerequisites

Before we begin building our expense tracker, let’s ensure you have the necessary tools and environment set up.

  • Node.js and npm: Ensure you have Node.js installed on your machine. You can download it here.
  • Create React App: We’ll be using Create React App for a quick setup. If you don’t have it installed, run the following command in your terminal:
npx create-react-app expense-tracker

This command creates a new React app named “expense-tracker”.

Navigate to Your Project

cd expense-tracker

Install Bootstrap

We will be using Bootstrap for designing. Let’s add it to the project using node

npm install bootstrap

Starting the Development Server

Now, let’s start the development server and see our default React app in action.

npm start

Visit http://localhost:3000 in your browser to ensure everything is set up correctly. You should see the default React welcome page.

In the next section, we’ll delve into the project structure and organization.

3. Setting Up Your React App

Now that we’ve ensured our environment is set up, let’s explore the project structure and set the foundation for our expense tracker.

Project Structure Overview

The project structure created by the Create React App is designed for efficiency and simplicity. Here are the key directories and files:

  • public: Static assets and the main HTML file.
  • src: React components and application logic.
  • App.js: The main component.
  • index.js: The entry point for rendering the app.

Customizing the Default App

Let’s start by customizing the default React app to fit our expense tracker project.

  • Open the src/App.js file in your code editor.
// src/App.js
import React from 'react';
import './App.css';

function App() {
  return (
    <div className="container">
      <header className="text-center mt-5 mb-5">
        <h1>Expense Tracker</h1>
      </header>
      {/* Your expense tracker components will go here */}
    </div>
  );
}

export default App;

Styling Your Expense Tracker

Open the src/App.css file and add Bootstrap styles:

/* src/App.css */
@import '~bootstrap/dist/css/bootstrap.min.css';

body {
  background-color: #f8f9fa;
}

.container {
  max-width: 800px;
}

In the next section, we’ll delve into designing the UI for our expense tracker.

Designing the UI

Creating an intuitive and user-friendly UI is crucial for an expense tracker. In this section, we’ll design the basic layout and components using Bootstrap.

We will have 3 main components.

  • Header Component: Displays information such as Balance, Income and Expenses
react js tutorial
  • Transactions Component: Lists detailed income and expense transactions with descriptions and amounts.
  • Add Transaction Form: User-friendly form for effortless addition of expenses or income, ensuring accurate financial tracking.

Header Component

Create a “components” directory within the “src” folder, and subsequently, generate a “Header.js” file for designing the header component.

import React from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCircleArrowDown, faCircleArrowUp, faWallet } from '@fortawesome/free-solid-svg-icons';

// Header component displays key financial information
const Header = ({ income, expense, balance }) => {
    return (
        // Bootstrap grid layout with three columns for Expense, Balance, and Income
        <div className='row mb-3'>
            {/* Expense column */}
            <div className='col-md-4 col-6 order-2'>
                {/* Bootstrap card for displaying expense */}
                <div className='card shadow'>
                    <div className='card-body'>
                        {/* Display expense amount */}
                        <h1 className='display-4'>${expense}</h1>
                        {/* Display expense icon and text */}
                        <h3><FontAwesomeIcon icon={faCircleArrowUp} color='red' /> Expense</h3>
                    </div>
                </div>
            </div>
            {/* Balance column */}
            <div className='col-md-4 col-12 mb-md-0 mb-3 order-md-3 order-1'>
                {/* Bootstrap card for displaying balance */}
                <div className='card shadow'>
                    <div className='card-body'>
                        {/* Display balance amount */}
                        <h1 className='display-4'>${balance}</h1>
                        {/* Display balance icon and text */}
                        <h3><FontAwesomeIcon icon={faWallet} color='brown' /> Balance</h3>
                    </div>
                </div>
            </div>
            {/* Income column */}
            <div className='col-md-4 col-6 order-4'>
                {/* Bootstrap card for displaying income */}
                <div className='card shadow'>
                    <div className='card-body'>
                        {/* Display income amount */}
                        <h1 className='display-4'>${income}</h1>
                        {/* Display income icon and text */}
                        <h3><FontAwesomeIcon icon={faCircleArrowDown} color='green' /> Income</h3>
                    </div>
                </div>
            </div>
        </div>
    );
}

export default Header;

TransactionList Component

Now, proceed to establish a “TransactionList.js” file within the “components” directory. This component is equipped with two props: “transactions” and a “deleteItem” function, facilitating the deletion of an item based on its ID. Additionally, an object named “expenseIcons” has been defined to simplify the accessibility of category icons.

import React from 'react';
import './style.css';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faBurger, faCar, faDollar, faEllipsisH, faFilm, faHeartbeat, faLightbulb, faShoppingBag, faTrash } from '@fortawesome/free-solid-svg-icons';

// TransactionList component displays a list of transactions with icons and details
const TransactionList = ({ transactions, deleteItem }) => {
    return (
        <>
            {/* Check if there are transactions */}
            {transactions?.length ? (
                <ul className="list-group transactions shadow">
                    {/* Map through transactions and create list items */}
                    {transactions.map(item => (
                        <li key={item.id} className="list-group-item d-flex justify-content-between align-items-center">
                            {/* Display transaction icon based on type */}
                            {expenseIcons[item.type]}
                            <div className="ms-3 me-auto">
                                {/* Display transaction description */}
                                <div className="fw-bold">{item.description}</div>
                                {/* Display transaction amount */}
                                {item.amount}
                            </div>
                            {/* Display delete icon and handle deleteItem function */}
                            <FontAwesomeIcon icon={faTrash} fontSize={24} cursor="pointer" onClick={() => deleteItem(item.id)} />
                        </li>
                    ))}
                </ul>
            ) : (
                // Display message if no transactions
                <div className='text-center text-italic no-record card shadow'>No Transactions Yet</div>
            )}
        </>
    );
}

export default TransactionList;

// Define expenseIcons object for mapping transaction types to icons
export const expenseIcons = {
    default: <FontAwesomeIcon icon={faDollar} fontSize={24} />,
    food: <FontAwesomeIcon icon={faBurger} fontSize={24} />,
    entertainment: <FontAwesomeIcon icon={faFilm} fontSize={24} />,
    transportation: <FontAwesomeIcon icon={faCar} fontSize={24} />,
    shopping: <FontAwesomeIcon icon={faShoppingBag} fontSize={24} />,
    health: <FontAwesomeIcon icon={faHeartbeat} fontSize={24} />,
    utilities: <FontAwesomeIcon icon={faLightbulb} fontSize={24} />,
    other: <FontAwesomeIcon icon={faEllipsisH} fontSize={24} />
}

Add Transaction

Create an “AddTransaction” component in React to seamlessly input transactions. This component employs an offcanvas design, adapting to the available balance and dynamically setting the transaction mode (income or expense). Upon submission, it triggers the onAddTransaction function, showcasing a brief success message and resetting the form.

import React, { useState } from 'react';
import { expenseIcons } from './TransactionList'; // Importing the expenseIcons object from TransactionList
import "./style.css"
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCheck, faSave } from '@fortawesome/free-solid-svg-icons';

// AddTransaction component allows users to input transactions and dynamically adjusts based on available balance
const AddTransaction = ({ onAddTransaction, balance }) => {
    const [success, setSuccess] = useState(false); // State to manage success message visibility
    const [formData, setFormData] = useState({
        id: new Date().getTime().toString(),
        amount: '',
        description: '',
        type: 'default',
        mode: balance <= 0 ? 'income' : 'expense' // Dynamically set transaction mode based on balance
    });

    // Handle changes in the form fields
    const handleChange = (e) => {
        const { id, value } = e.target;
        setFormData((prevData) => ({
            ...prevData,
            [id]: value
        }));
    };

    // Handle form submission
    const handleSubmit = (e) => {
        e.preventDefault();
        onAddTransaction(formData); // Trigger onAddTransaction function with form data
        setSuccess(true); // Show success message
        setFormData({
            id: new Date().getTime().toString(),
            amount: '',
            description: '',
            type: '',
            mode: balance <= 0 ? 'income' : 'expense' // Reset form data with adjusted mode
        });
        setTimeout(() => {
            setSuccess(false);
        }, 800); // Hide success message after a brief delay
    };

    return (
        <div>
            <div className='text-center'>
                <button
                    className="btn btn-dark col-md-4 col-12 my-3"
                    type="button"
                    data-bs-toggle="offcanvas"
                    data-bs-target="#bottomOffcanvas"
                    aria-controls="bottomOffcanvas"
                >
                    + Add Transaction
                </button>
            </div>

            <div
                className={`offcanvas offcanvas-bottom`}
                tabIndex="-1"
                id="bottomOffcanvas"
                aria-labelledby="bottomOffcanvasLabel"
                style={{ height: "auto" }}
            >
                <div className="offcanvas-header">
                    <h5 className="offcanvas-title" id="bottomOffcanvasLabel">
                        Add Transaction
                    </h5>
                    <button
                        type="button"
                        className="btn-close text-reset"
                        data-bs-dismiss="offcanvas"
                        aria-label="Close"
                    ></button>
                </div>
                <div className="offcanvas-body">
                    <form onSubmit={handleSubmit}>
                        <div className="mb-3">
                            <label htmlFor="type" className="form-label">
                                Type
                            </label>
                            {/* Dropdown to select transaction type (income/expense) */}
                            <select
                                className="form-select"
                                id="mode"
                                value={formData.mode}
                                onChange={handleChange}
                                required
                            >
                                <option value="" disabled>
                                    Select Type
                                </option>
                                {/* Options for expense and income */}
                                <option key={"type1"} value={"expense"} disabled={balance <= 0}>
                                    Expense
                                </option>
                                <option key={"type2"} value={"income"}>
                                    Income
                                </option>
                            </select>
                        </div>
                        <div className="mb-3">
                            <label htmlFor="amount" className="form-label">
                                Amount
                            </label>
                            {/* Input field for the transaction amount */}
                            <input
                                type="number"
                                className="form-control"
                                id="amount"
                                max={formData.mode === "expense" ? balance : Infinity}
                                value={formData.amount}
                                onChange={handleChange}
                                required
                            />
                        </div>
                        <div className="mb-3">
                            <label htmlFor="description" className="form-label">
                                Description
                            </label>
                            {/* Input field for the transaction description */}
                            <input
                                type="text"
                                className="form-control"
                                id="description"
                                value={formData.description}
                                onChange={handleChange}
                                required
                            />
                        </div>
                        <div className="mb-3">
                            <label htmlFor="type" className="form-label">
                                Category
                            </label>
                            {/* Dropdown to select transaction category */}
                            <select
                                className="form-select"
                                id="type"
                                value={formData.mode === "income" ? "default" : formData.type}
                                onChange={handleChange}
                                disabled={formData.mode === "income"}
                                required
                            >
                                <option value="" disabled>
                                    Select Category
                                </option>
                                {/* Options for different expense categories */}
                                {Object.keys(expenseIcons).map((type) => (
                                    <option key={type} value={type}>
                                        {type.charAt(0).toUpperCase() + type.slice(1)}
                                    </option>
                                ))}
                            </select>
                        </div>
                        {/* Button to submit the form */}
                        <button type="submit" className="btn btn-dark">
                            <FontAwesomeIcon icon={faSave} /> Save
                        </button>
                        {/* Success message displayed after a successful transaction */}
                        <span className={`ms-3 fade ${success ? 'show' : ''}`}>
                            <FontAwesomeIcon icon={faCheck} /> Transaction Added Successfully!
                        </span>
                    </form>
                </div>
            </div>
        </div>
    );
};

export default AddTransaction;

Add Styles

Create a style.css file inside components directory and update the contents to this.

.transactions {
    height: calc(100vh - 400px);
    margin-bottom: 10px;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overflow-x: hidden;
}

@media (max-width:480px) {

    .transactions,
    .no-record {
        height: calc(100vh - 480px) !important;
    }
}

.no-record {
    height: calc(100vh - 400px) !important;
    padding: 10px;
    font-size: large;
    font-style: italic;
}

.transactions::-webkit-scrollbar {
    width: 6px;
}

.transactions::-webkit-scrollbar-track {
    box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.2);
    border-radius: 10px;
}

.transactions::-webkit-scrollbar-thumb {
    background-color: lightgray;
    border-radius: 10px;
}

Combining Components and Implementing Local Storage

In this segment, we’ll consolidate the various components into a single file named “ExpenseTracker.js” and integrate it into the main file, App.js.

import React, { useEffect, useState } from 'react';
import Header from './Header';
import TransactionList from './TransactionList';
import AddTransaction from './AddTransaction';

// ExpenseTracker component manages the entire expense tracking application
const ExpenseTracker = () => {
    // State variables to track income, expense, and transaction history
    const [income, setIncome] = useState(0);
    const [expense, setExpense] = useState(0);
    const [transactions, setTransactions] = useState([]);

    // Load transactions from local storage on component mount
    useEffect(() => {
        const items = JSON.parse(localStorage.getItem("transactions"));
        if (items?.length) setTransactions(items);
    }, []);

    // Update income and expense totals whenever transactions change
    useEffect(() => {
        if (transactions) {
            let totalExpense = 0;
            let totalIncome = 0;

            transactions.forEach(item => {
                if (item.mode === "expense") {
                    totalExpense += Number(item.amount);
                } else if (item.mode === "income") {
                    totalIncome += Number(item.amount);
                }
            });

            setExpense(totalExpense);
            setIncome(totalIncome);
        }
    }, [transactions, setExpense, setIncome]);

    // Function to add a new transaction to the list and update local storage
    const onAddTransaction = (formData) => {
        let data = [formData, ...transactions];
        setTransactions(data);
        localStorage.setItem("transactions", JSON.stringify(data));
    }

    // Function to delete a transaction by ID and update local storage
    const deleteTransaction = (id) => {
        let newItems = transactions.filter(x => x.id !== id);
        setTransactions(newItems);
        localStorage.setItem("transactions", JSON.stringify(newItems));
    }

    // Render the main layout with Header, TransactionList, and AddTransaction components
    return (
        <div className='container-fluid'>
            <Header income={income} expense={expense} balance={income - expense} />
            <TransactionList transactions={transactions} deleteItem={deleteTransaction} />
            <AddTransaction onAddTransaction={onAddTransaction} balance={income - expense} />
        </div>
    );
}

export default ExpenseTracker;

This ExpenseTracker component serves as the core of the expense tracking application. It manages state variables for income, expense, and transactions, fetches and updates data from local storage, and renders the Header, TransactionList, and AddTransaction components. The useEffect hooks handle initial data loading and updating totals based on transactions. The onAddTransaction and deleteTransaction functions modify the transaction list and update local storage accordingly.

Finishing it up

Now we can just add our Expense Tracker component to App.js and we are done.

import React from 'react';
import './App.css';
import ExpenseTracker from './components/ExpenseTracker';

function App() {
  return (
    <div className="container">
      <header className="text-center my-5">
        <h1>Expense Tracker</h1>
      </header>
      <ExpenseTracker />
    </div>
  );
}

export default App;

Conclusion

Congratulations on completing this React JS tutorial! You’ve successfully built a responsive expense tracker with React, utilizing local storage for a seamless user experience.

To take your project to the next level, consider adding features such as expense editing or deletion, implementing visual charts, and exploring advanced React concepts. Enhance the design for a polished look and transform it into a Progressive Web App (PWA) for mobile installation. Follow this guide: Unlock Ultra-Fast Performance with the Power of PWA Apps.

Thank you for following along with this tutorial. Your coding journey doesn’t end here! Feel free to comment with your thoughts, share this tutorial with fellow developers, and hit the like button if you found it helpful. Happy coding!

2 thoughts on “React JS tutorial: Building an expense tracker with local storage”

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top