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
- 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!
It was very helpful. thank you for this great article
Nice article.