Build an Expense Tracker with React
Build ExpenseFlow, a React app that tracks spending with a starting balance, category breakdowns, and real-time summaries.
Introduction
30 Second Summary
Keeping track of where your money goes is one of those tasks everyone knows they should do but rarely sticks with. The right tool makes all the difference.
In this project, you will build ExpenseFlow, a fully functional expense tracker using React. You will create components that handle forms, display lists, track a starting balance, and calculate spending totals in real time.
What You'll Build
A React expense tracker app called ExpenseFlow that lets you set a starting balance, add and delete expenses by category, and view a live-updating summary of your remaining budget.
By the end of this project, you'll have:
- An ExpenseForm component with controlled inputs for description, amount, and category.
- An ExpenseList component that renders recent expenses with category badges and delete functionality.
- An ExpenseSummary component showing starting balance, total expenses, remaining balance, and per-category breakdowns.
- Secret Mission: Add filtering by category and a date field with date-range filtering.
Are there any prerequisites?
This project assumes intermediate JavaScript knowledge. You should be comfortable with functions, arrays, and objects before starting.
Before We Start
Before We Start...
Set Up Your React Project with Vite
Every React app needs a project structure with the right tools to compile JSX code into something the browser understands.
In this step, you'll scaffold a React project using Vite, a fast build tool that handles all the compilation for you. You'll also clean up the starter files so you have a blank canvas to work with.
In this step, get ready to:
- Scaffold a new React project with Vite.
- Clean up the starter template files.
- Start the development server and confirm hot-reload works.
Scaffold a new React project
Vite provides a scaffold command that generates all the files and configuration a React project needs. You just need to run one command and Vite does the rest.
- Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Terminal, and pressing Enter.
- Move to your Desktop by running this command:
cd ~/Desktop
- Scaffold your React project by running this command:
npm create vite@latest expense-tracker -- --template react
What does this command do?
- npm create vite@latest runs the latest version of Vite's project scaffolding tool.
- expense-tracker is the name of your project folder.
- -- --template react tells Vite to use the React template. The extra -- is required by npm to pass flags through to the scaffold tool.
- Navigate into the new project folder and install dependencies by running these commands:
cd expense-tracker
npm install
You should see a message like "added X packages" in your terminal. This means all the project dependencies are installed.
Seeing an error from the scaffold command?
- Make sure you have Node.js version 20.19 or higher installed. Run node --version to check.
- If you see a network error, check your internet connection and try again.
- If you see a folder already exists error, delete the existing expense-tracker folder on your Desktop and retry.
Still stuck? Help me troubleshoot this Vite scaffold error.
💡 Why Vite instead of Create React App?
Create React App was the original scaffolding tool for React projects, but it has been officially deprecated. The React team no longer recommends it.
Vite is faster, lighter, and actively maintained. It gives you instant hot-reload during development and quick production builds.
Clean up the starter files
The Vite template includes demo content like hero images, logos, and sample code. You need to clear these out so your app starts as a blank canvas.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Select the expense-tracker folder on your Desktop, then click Open.
- In the Explorer sidebar, expand the src folder.
- Click on App.css to open it.
- Select all the content with Cmd+A (macOS) or Ctrl+A (Windows) and delete it, leaving the file empty.
- Click on index.css in the sidebar to open it.
- Select all and delete the content, leaving this file empty too.
- Right-click the assets folder inside src and click Delete to remove it.
Now replace the demo code in App.jsx with a minimal component.
- Click on App.jsx in the sidebar to open it.
- Select all the existing content and replace it with the following code:
function App() {
return (
<div>
<h1>ExpenseFlow</h1>
</div>
)
}
export default App
What does this code do?
- function App() defines a React component called App. Components are the building blocks of every React app.
- The return statement outputs JSX, which looks like HTML but lives inside your JavaScript.
- export default App makes this component available to other files that import it.
- Save App.jsx with Cmd+S (macOS) or Ctrl+S (Windows).
- Also save App.css and index.css if you haven't already.
Not sure if your files are saved?
Look at the file tabs in VS Code. A white dot on a tab means that file has unsaved changes. Click on any tab with a dot and press Cmd+S (macOS) or Ctrl+S (Windows) to save it.
Help me check my files are set up correctly.
✔️ Awesome, I've got everything!
Great. Make sure all three files are saved before moving on.
ⓧ I'd like to double check the full code
Here is what src/App.jsx should look like:
function App() {
return (
<div>
<h1>ExpenseFlow</h1>
</div>
)
}
export default App
Both src/App.css and src/index.css should be completely empty files with no content.
The src/main.jsx file should remain unchanged from the template:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
Start the development server
Vite includes a development server that automatically refreshes your browser whenever you save a file. This is called hot-reload.
- Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, same on macOS and Windows).
- Start the development server by running:
npm run dev
- You should see output in the terminal showing a local URL, typically http://localhost:5173.
- Hold Cmd (macOS) or Ctrl (Windows) and click the URL in the terminal to open it in your browser.
You should see "ExpenseFlow" displayed as a heading in your browser.
What is hot-reload?
Hot-reload means Vite watches your files for changes. Every time you save a file, the browser updates instantly without needing a manual refresh.
Try it now. Change the heading text in App.jsx to something else, save, and watch the browser update by itself. Then change it back to "ExpenseFlow" before continuing.
🙋♀️ Don't see your heading in the browser?
- Make sure you saved App.jsx after pasting the new code.
- Check that your terminal is still running npm run dev without errors. If it crashed, stop it with Ctrl+C and run npm run dev again.
- Confirm you are opening the correct URL shown in your terminal output.
My browser isn't showing the ExpenseFlow heading. Help me debug.
Your React project is scaffolded, cleaned up, and running with hot-reload. Next up, you'll build your first real component: the expense form.
Build the ExpenseForm Component
Your project scaffold is running and showing a heading. Now it is time to build the first real piece of your app.
Every expense tracker needs a way for users to type in what they spent. In React, you build input forms using controlled components. Each input's value is tied to a piece of state, so React always knows what the user has typed.
In this step, get ready to:
- Create an ExpenseForm component with controlled inputs for description, amount, and category.
- Handle form submission with validation and state reset.
- Wire the form into App so new expenses are stored in state.
Create the ExpenseForm component with controlled inputs
React components live in their own files. You will create a components folder to keep them organized.
- In VS Code, create a new folder called components inside the src folder.
- Inside src/components, create a new file called ExpenseForm.jsx.
- Paste the following code into ExpenseForm.jsx:
import { useState } from 'react'
function ExpenseForm({ onAddExpense }) {
const [description, setDescription] = useState('')
const [amount, setAmount] = useState('')
const [category, setCategory] = useState('Food')
function handleSubmit(e) {
e.preventDefault()
if (!description.trim() || !amount || Number(amount) <= 0) {
alert('Please fill in all fields with a valid amount.')
return
}
const newExpense = {
id: Date.now(),
description: description.trim(),
amount: Number(amount),
category,
}
onAddExpense(newExpense)
setDescription('')
setAmount('')
setCategory('Food')
}
return (
<form className="expense-form" onSubmit={handleSubmit}>
<h2>Add Expense</h2>
<div className="form-group">
<label htmlFor="description">Description</label>
<input
id="description"
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="e.g. Lunch"
/>
</div>
<div className="form-group">
<label htmlFor="amount">Amount ($)</label>
<input
id="amount"
type="number"
step="0.01"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.00"
/>
</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="Transport">Transport</option>
<option value="Entertainment">Entertainment</option>
<option value="Other">Other</option>
</select>
</div>
<button type="submit">Add Expense</button>
</form>
)
}
export default ExpenseForm
What does this code do?
This component uses three useState hooks to track what the user types. Each input's value is bound to a state variable, and each onChange handler updates that state. This is the controlled component pattern. React owns the input data at all times.
The category input uses a <select> dropdown pre-filled with four options: Food, Transport, Entertainment, and Other. Its state defaults to 'Food' so the form always has a valid selection.
- Save ExpenseForm.jsx.
You will not see a change in the browser yet. The component needs to be imported into App.jsx before it renders on the page.
Handle form submission
The handleSubmit function inside your component runs every time the user clicks the submit button. It does four things in sequence.
What does handleSubmit do?
- e.preventDefault() stops the browser from reloading the page (the default behavior of HTML forms).
- The if block checks that description is not empty, amount exists, and amount is a positive number. If validation fails, an alert tells the user what went wrong.
- Date.now() generates a unique numeric ID for each expense. This gives every item a stable key for React's list rendering later.
- After calling onAddExpense(newExpense) to pass the data up to the parent, the function resets all three state variables so the form is blank and ready for the next entry.
💡 What is onAddExpense?
onAddExpense is a prop. The parent component (App) passes a function down so the child (ExpenseForm) can send data back up. This is how React components communicate: data flows down through props, and events flow up through callback functions.
Wire it up in App
The form component is ready, but it is not visible yet. You need to import it into App.jsx and give it the onAddExpense prop it expects.
- Switch back to src/App.jsx in VS Code.
- Replace the entire file contents with this code:
import { useState } from 'react'
import ExpenseForm from './components/ExpenseForm.jsx'
import './App.css'
function App() {
// Store all expenses in an array
const [expenses, setExpenses] = useState([])
// Track the user's starting balance
const [startingBalance, setStartingBalance] = useState(0)
function addExpense(expense) {
setExpenses([...expenses, expense])
}
return (
<div className="app">
<header className="app-header">
<h1>ExpenseFlow</h1>
</header>
<div className="balance-card">
<label htmlFor="starting-balance">Starting Balance ($)</label>
<input
id="starting-balance"
type="number"
step="0.01"
value={startingBalance}
onChange={(e) => setStartingBalance(Number(e.target.value))}
placeholder="0.00"
/>
</div>
<div className="container">
<ExpenseForm onAddExpense={addExpense} />
</div>
</div>
)
}
export default App
What does this code do?
- useState([]) creates an empty array to hold all the expenses the user adds over time.
- useState(0) creates a startingBalance state that the user can update via an input field.
- addExpense takes a new expense object and creates a new array with all existing expenses plus the new one. The spread operator (...expenses) copies the old array without mutating it.
- The balance card gives users a place to enter their budget before adding expenses. The summary component (built later) uses this to calculate remaining balance.
- Save App.jsx.
- Switch to your browser. You should see the "ExpenseFlow" header, a Starting Balance input, and the "Add Expense" form with fields for Description, Amount, and Category.
- Fill in the form and click Add Expense. Open your browser's developer console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) to confirm no errors appear.
Verify with React DevTools
If you have the React DevTools browser extension installed, click the Components tab in DevTools. Select the App component and check its hooks section. After submitting the form, you should see the expenses array grow with each new entry.
🙋♀️ Form not showing up?
- Check that your file is named ExpenseForm.jsx (capital E, capital F) inside the src/components/ folder. The import path in App.jsx must match exactly.
- Make sure you saved both files. Unsaved changes do not trigger a hot reload.
- Look at your terminal for error messages. A red error overlay in the browser usually means a typo in your JSX or a missing closing bracket.
Why isn't my ExpenseForm rendering in the browser?
✔️ Awesome, I've got everything!
Great. Double check you have saved both ExpenseForm.jsx and App.jsx.
ⓧ I'd like to double check the full code
Here is src/components/ExpenseForm.jsx:
import { useState } from 'react'
function ExpenseForm({ onAddExpense }) {
const [description, setDescription] = useState('')
const [amount, setAmount] = useState('')
const [category, setCategory] = useState('Food')
function handleSubmit(e) {
e.preventDefault()
if (!description.trim() || !amount || Number(amount) <= 0) {
alert('Please fill in all fields with a valid amount.')
return
}
const newExpense = {
id: Date.now(),
description: description.trim(),
amount: Number(amount),
category,
}
onAddExpense(newExpense)
setDescription('')
setAmount('')
setCategory('Food')
}
return (
<form className="expense-form" onSubmit={handleSubmit}>
<h2>Add Expense</h2>
<div className="form-group">
<label htmlFor="description">Description</label>
<input
id="description"
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="e.g. Lunch"
/>
</div>
<div className="form-group">
<label htmlFor="amount">Amount ($)</label>
<input
id="amount"
type="number"
step="0.01"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.00"
/>
</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="Transport">Transport</option>
<option value="Entertainment">Entertainment</option>
<option value="Other">Other</option>
</select>
</div>
<button type="submit">Add Expense</button>
</form>
)
}
export default ExpenseForm
Here is src/App.jsx:
import { useState } from 'react'
import ExpenseForm from './components/ExpenseForm.jsx'
import './App.css'
function App() {
const [expenses, setExpenses] = useState([])
const [startingBalance, setStartingBalance] = useState(0)
function addExpense(expense) {
setExpenses([...expenses, expense])
}
return (
<div className="app">
<header className="app-header">
<h1>ExpenseFlow</h1>
</header>
<div className="balance-card">
<label htmlFor="starting-balance">Starting Balance ($)</label>
<input
id="starting-balance"
type="number"
step="0.01"
value={startingBalance}
onChange={(e) => setStartingBalance(Number(e.target.value))}
placeholder="0.00"
/>
</div>
<div className="container">
<ExpenseForm onAddExpense={addExpense} />
</div>
</div>
)
}
export default App
Your form is collecting data and storing it in state. Next up, you will build the ExpenseList component to display those expenses on the page.
Build the ExpenseList Component
Your expense form is working and adding data to state. Now you need a way to display those expenses on screen so users can see what they've tracked.
In this step, you'll build an ExpenseList component that takes the expenses array and renders each item dynamically. You'll also add delete functionality so users can remove entries they no longer need.
In this step, get ready to:
- Create the ExpenseList component with list rendering.
- Add conditional rendering for empty states.
- Wire up a delete function to remove expenses.
Create the ExpenseList component
React's .map() method is the standard pattern for rendering a list of items from an array. Each item in the array gets transformed into a piece of JSX that React renders on screen.
- Create a new file at src/components/ExpenseList.jsx and paste in the following code:
function ExpenseList({ expenses, onDeleteExpense }) {
return (
<div className="expense-list">
<h2>Recent Expenses</h2>
<ul>
{expenses.map((expense) => (
<li key={expense.id} className="expense-item">
<div className="expense-details">
<span className="expense-description">{expense.description}</span>
<span className={`expense-category badge-${expense.category.toLowerCase()}`}>
{expense.category}
</span>
</div>
<div className="expense-actions">
<span className="expense-amount">
${expense.amount.toFixed(2)}
</span>
<button
className="delete-btn"
onClick={() => onDeleteExpense(expense.id)}
title="Delete expense"
>
×
</button>
</div>
</li>
))}
</ul>
</div>
)
}
export default ExpenseList
What does this code do?
The component receives two props from its parent: expenses (the array of expense objects) and onDeleteExpense (a callback function to handle deletion).
- expenses.map() loops through every expense and returns a <li> element for each one.
- The category badge uses a template literal class (badge-${expense.category.toLowerCase()}) so each category gets its own color via CSS.
- The delete button uses a × character instead of text, giving it a compact trash-icon appearance.
- key={expense.id} gives React a unique identifier for each list item so it can efficiently update the DOM when items change.
- Save ExpenseList.jsx.
- Confirm the file appears in the src/components/ folder in your editor's sidebar, alongside ExpenseForm.jsx.
You won't see this component on screen yet. The next substep wires it into your app.
Add conditional rendering for empty state
When a user first opens the app, the expenses array is empty. Rendering an empty list with no items looks broken. Conditional rendering lets you show different content based on the current state of your data.
- In ExpenseList.jsx, find this line at the top of the function:
function ExpenseList({ expenses, onDeleteExpense }) {
- Add the following code directly below that line, above the existing return statement:
if (expenses.length === 0) {
return <p className="empty-state">No expenses yet. Add one above!</p>
}
What does this code do?
This is an early return pattern. If the expenses array has zero items, the function returns a friendly message and never reaches the list-rendering code below it. Once the user adds their first expense, expenses.length becomes 1 and React renders the full list instead.
- Save ExpenseList.jsx.
You won't see a change in the browser yet. The next substep connects this component to your app.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
function ExpenseList({ expenses, onDeleteExpense }) {
if (expenses.length === 0) {
return <p className="empty-state">No expenses yet. Add one above!</p>
}
return (
<div className="expense-list">
<h2>Recent Expenses</h2>
<ul>
{expenses.map((expense) => (
<li key={expense.id} className="expense-item">
<div className="expense-details">
<span className="expense-description">{expense.description}</span>
<span className={`expense-category badge-${expense.category.toLowerCase()}`}>
{expense.category}
</span>
</div>
<div className="expense-actions">
<span className="expense-amount">
${expense.amount.toFixed(2)}
</span>
<button
className="delete-btn"
onClick={() => onDeleteExpense(expense.id)}
title="Delete expense"
>
×
</button>
</div>
</li>
))}
</ul>
</div>
)
}
export default ExpenseList
Wire up ExpenseList in App.jsx
The ExpenseList component exists but your app doesn't know about it yet. You need to import it, create a delete handler, and render it with the right props.
- Switch back to src/App.jsx.
- Find this import line near the top:
import ExpenseForm from './components/ExpenseForm.jsx'
- Add this new import directly below it:
import ExpenseList from './components/ExpenseList.jsx'
- Next, find the closing brace of the addExpense function:
function addExpense(expense) {
setExpenses([...expenses, expense])
}
- Add the deleteExpense function directly below it:
function deleteExpense(id) {
setExpenses(expenses.filter((expense) => expense.id !== id))
}
What does deleteExpense do?
The .filter() method creates a new array that includes every expense EXCEPT the one whose id matches the one being deleted. React sees the new array and re-renders the list without that item.
This is the standard React pattern for deleting from a list. You never mutate the existing array directly. Instead, you create a new one and pass it to the state setter.
- Finally, find this line inside the return statement:
<ExpenseForm onAddExpense={addExpense} />
- Add the ExpenseList component directly below it:
<ExpenseList expenses={expenses} onDeleteExpense={deleteExpense} />
How are props flowing here?
App passes two props down to ExpenseList: the expenses array (the data to display) and the deleteExpense function (the action to take when the user clicks Delete).
This pattern of keeping state in a parent and passing handlers to children is called "lifting state up." App owns the data and the children just display it and report user actions back.
- Save App.jsx.
- Return to your browser. You should see the message "No expenses yet. Add one above!" below the form.
- Fill in the form and click Add Expense. Your expense should appear in the list with its description, category, amount, and a Delete button.
- Click Delete on the expense. It should disappear and the empty-state message should return.
Not seeing the expense list?
- Make sure you saved both ExpenseList.jsx and App.jsx. Unsaved changes won't trigger a re-render.
- Check the browser console for errors. A missing import or a typo in the component name will show a clear error message.
- Confirm the import path matches your file location exactly: ./components/ExpenseList.jsx.
Help me debug why my ExpenseList isn't rendering.
✔️ Awesome, I've got everything!
Great. Double check you've saved both files before moving on.
ⓧ I'd like to double check the full code
import { useState } from 'react'
import ExpenseForm from './components/ExpenseForm.jsx'
import ExpenseList from './components/ExpenseList.jsx'
import './App.css'
function App() {
const [expenses, setExpenses] = useState([])
const [startingBalance, setStartingBalance] = useState(0)
function addExpense(expense) {
setExpenses([...expenses, expense])
}
function deleteExpense(id) {
setExpenses(expenses.filter((expense) => expense.id !== id))
}
return (
<div className="app">
<header className="app-header">
<h1>ExpenseFlow</h1>
</header>
<div className="balance-card">
<label htmlFor="starting-balance">Starting Balance ($)</label>
<input
id="starting-balance"
type="number"
step="0.01"
value={startingBalance}
onChange={(e) => setStartingBalance(Number(e.target.value))}
placeholder="0.00"
/>
</div>
<div className="container">
<ExpenseForm onAddExpense={addExpense} />
<ExpenseList expenses={expenses} onDeleteExpense={deleteExpense} />
</div>
</div>
)
}
export default App
Your expenses now appear on screen and can be removed with a click. Next up, you'll build a summary component that calculates totals and breaks down spending by category.
Build the ExpenseSummary Component
Your expenses are tracked and listed. But a good expense tracker should give you a clear picture of where your money is going.
In this step, you'll build an ExpenseSummary component that shows your starting balance, total expenses, remaining balance, and a per-category breakdown. You'll also add CSS to give the whole app the polished ExpenseFlow look.
In this step, get ready to:
- Create an ExpenseSummary component that calculates starting balance, total expenses, remaining balance, and category breakdowns.
- Wire ExpenseSummary into your app layout.
- Style the entire app with the ExpenseFlow purple/blue theme.
Create the ExpenseSummary component
The summary component needs the expenses array and the starting balance. It computes three things: total expenses, remaining balance, and a per-category breakdown. JavaScript's .reduce() method is perfect for these tasks.
- Create a new file called ExpenseSummary.jsx inside your src/components/ folder.
- Add the following code to build the summary component:
function ExpenseSummary({ expenses, startingBalance }) {
// Calculate the grand total of all expenses
const totalExpenses = expenses.reduce((sum, expense) => sum + expense.amount, 0)
// Calculate remaining balance
const remainingBalance = startingBalance - totalExpenses
// Group expenses by category and sum each category's total
const categoryTotals = expenses.reduce((totals, expense) => {
totals[expense.category] = (totals[expense.category] || 0) + expense.amount
return totals
}, {})
return (
<div className="expense-summary">
<h2>Summary</h2>
<div className="summary-row">
<span>Starting Balance</span>
<span className="summary-value">${startingBalance.toFixed(2)}</span>
</div>
<div className="summary-row">
<span>Total Expenses</span>
<span className="summary-value expenses-total">-${totalExpenses.toFixed(2)}</span>
</div>
<div className="summary-row remaining">
<span>Remaining Balance</span>
<span className={`summary-value ${remainingBalance < 0 ? 'negative' : ''}`}>
${remainingBalance.toFixed(2)}
</span>
</div>
{Object.keys(categoryTotals).length > 0 && (
<div className="category-breakdown">
<h3>By Category</h3>
<ul>
{Object.entries(categoryTotals).map(([category, amount]) => (
<li key={category} className={`badge-${category.toLowerCase()}`}>
<span>{category}</span>
<span>${amount.toFixed(2)}</span>
</li>
))}
</ul>
</div>
)}
</div>
)
}
export default ExpenseSummary
What does this code do?
This component uses .reduce() twice and simple subtraction to derive useful data from the expenses array.
- The first .reduce() adds up every expense's amount into a single totalExpenses number, starting from 0.
- remainingBalance subtracts total expenses from the starting balance. If the user overspends, this goes negative and the class negative is applied for red styling.
- The second .reduce() builds a categoryTotals object where each key is a category name and each value is that category's total spend.
- The conditional render (Object.keys(categoryTotals).length > 0 &&) only shows the breakdown when there are expenses to display.
💡 What is .reduce()?
The .reduce() method processes an array one item at a time and accumulates a single result. You provide a callback and an initial value. Each iteration updates the accumulator and passes it to the next.
Think of it like a running tally. You start at 0, and for each expense you add its amount to the tally.
- Save ExpenseSummary.jsx.
Seeing a syntax error?
- Check that the curly braces inside the JSX return match up. The conditional render {Object.keys(categoryTotals).length > 0 && (...)} has nested parentheses that are easy to misplace.
- Make sure the export default ExpenseSummary line is at the bottom of the file, outside the function.
Why is my ExpenseSummary component not rendering correctly?
✔️ Awesome, I've got everything!
Great. Make sure you've saved the file before moving on.
ⓧ I'd like to double check the full code
function ExpenseSummary({ expenses, startingBalance }) {
const totalExpenses = expenses.reduce((sum, expense) => sum + expense.amount, 0)
const remainingBalance = startingBalance - totalExpenses
const categoryTotals = expenses.reduce((totals, expense) => {
totals[expense.category] = (totals[expense.category] || 0) + expense.amount
return totals
}, {})
return (
<div className="expense-summary">
<h2>Summary</h2>
<div className="summary-row">
<span>Starting Balance</span>
<span className="summary-value">${startingBalance.toFixed(2)}</span>
</div>
<div className="summary-row">
<span>Total Expenses</span>
<span className="summary-value expenses-total">-${totalExpenses.toFixed(2)}</span>
</div>
<div className="summary-row remaining">
<span>Remaining Balance</span>
<span className={`summary-value ${remainingBalance < 0 ? 'negative' : ''}`}>
${remainingBalance.toFixed(2)}
</span>
</div>
{Object.keys(categoryTotals).length > 0 && (
<div className="category-breakdown">
<h3>By Category</h3>
<ul>
{Object.entries(categoryTotals).map(([category, amount]) => (
<li key={category} className={`badge-${category.toLowerCase()}`}>
<span>{category}</span>
<span>${amount.toFixed(2)}</span>
</li>
))}
</ul>
</div>
)}
</div>
)
}
export default ExpenseSummary
Add ExpenseSummary to your app
The component exists but your app doesn't know about it yet. You need to import it and render it inside your layout.
- In src/App.jsx, find the import section at the top of the file:
import { useState } from 'react'
import ExpenseForm from './components/ExpenseForm.jsx'
import ExpenseList from './components/ExpenseList.jsx'
import './App.css'
- Add a new import for ExpenseSummary between the ExpenseList import and the ./App.css import, so the imports look like this:
import { useState } from 'react'
import ExpenseForm from './components/ExpenseForm.jsx'
import ExpenseList from './components/ExpenseList.jsx'
import ExpenseSummary from './components/ExpenseSummary.jsx'
import './App.css'
- Now find the return statement inside your App function. Locate this line:
<ExpenseForm onAddExpense={addExpense} />
<ExpenseList expenses={expenses} onDeleteExpense={deleteExpense} />
- Add <ExpenseSummary expenses={expenses} startingBalance={startingBalance} /> between ExpenseForm and ExpenseList, so the section looks like this:
<ExpenseForm onAddExpense={addExpense} />
<ExpenseSummary expenses={expenses} startingBalance={startingBalance} />
<ExpenseList expenses={expenses} onDeleteExpense={deleteExpense} />
- Save App.jsx.
- Check your browser. You should see a Summary section showing Starting Balance, Total Expenses, and Remaining Balance between the form and the expense list.
Don't see the summary?
- Make sure the import path is exactly ./components/ExpenseSummary.jsx. A typo in the path causes a silent failure.
- Confirm you passed the expenses prop: <ExpenseSummary expenses={expenses} />. Without it, the component receives undefined and crashes.
Why isn't my ExpenseSummary showing up in the browser?
✔️ Awesome, I've got everything!
Great. Double check you've saved the file.
ⓧ I'd like to double check the full code
import { useState } from 'react'
import ExpenseForm from './components/ExpenseForm.jsx'
import ExpenseList from './components/ExpenseList.jsx'
import ExpenseSummary from './components/ExpenseSummary.jsx'
import './App.css'
function App() {
const [expenses, setExpenses] = useState([])
const [startingBalance, setStartingBalance] = useState(0)
function addExpense(expense) {
setExpenses([...expenses, expense])
}
function deleteExpense(id) {
setExpenses(expenses.filter((expense) => expense.id !== id))
}
return (
<div className="app">
<header className="app-header">
<h1>ExpenseFlow</h1>
</header>
<div className="balance-card">
<label htmlFor="starting-balance">Starting Balance ($)</label>
<input
id="starting-balance"
type="number"
step="0.01"
value={startingBalance}
onChange={(e) => setStartingBalance(Number(e.target.value))}
placeholder="0.00"
/>
</div>
<div className="container">
<ExpenseForm onAddExpense={addExpense} />
<ExpenseSummary expenses={expenses} startingBalance={startingBalance} />
<ExpenseList expenses={expenses} onDeleteExpense={deleteExpense} />
</div>
</div>
)
}
export default App
Style the app
The functionality is complete but the app still looks unstyled. Adding CSS gives each section a distinct visual identity so the form, summary, and list are easy to scan.
- Open src/index.css and replace its contents with this global reset:
body {
margin: 0;
padding: 0;
background: #ffffff;
}
What does this do?
Browsers add default margin and padding to the body element. This reset removes that spacing so your layout starts from the edges of the viewport.
- Save index.css.
- Now open src/App.css and replace its contents with the following styles:
/* App layout */
.app {
max-width: 600px;
margin: 0 auto;
padding: 2rem 1rem;
font-family: system-ui, -apple-system, sans-serif;
}
/* Header */
.app-header {
background: #4f46e5;
color: white;
padding: 1rem 1.5rem;
border-radius: 8px;
margin-bottom: 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.app-header h1 {
margin: 0;
font-size: 1.5rem;
}
/* Starting Balance card */
.balance-card {
background: #f1f5f9;
padding: 1.25rem 1.5rem;
border-radius: 8px;
margin-bottom: 1.5rem;
border: 1px solid #e2e8f0;
}
.balance-card label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: #334155;
}
.balance-card input {
width: 100%;
padding: 0.5rem;
border: 1px solid #cbd5e1;
border-radius: 4px;
font-size: 1rem;
box-sizing: border-box;
}
/* Container for components */
.container {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
/* Expense Form */
.expense-form {
background: #f8fafc;
padding: 1.5rem;
border-radius: 8px;
border: 1px solid #e2e8f0;
}
.expense-form h2 {
margin-top: 0;
margin-bottom: 1rem;
color: #1e293b;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.25rem;
font-weight: 500;
color: #475569;
}
.form-group input,
.form-group select {
width: 100%;
padding: 0.5rem;
border: 1px solid #cbd5e1;
border-radius: 4px;
font-size: 1rem;
box-sizing: border-box;
}
.expense-form button {
width: 100%;
padding: 0.75rem;
background: #4f46e5;
color: white;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
}
.expense-form button:hover {
background: #4338ca;
}
/* Expense Summary */
.expense-summary {
background: #f8fafc;
padding: 1.5rem;
border-radius: 8px;
border: 1px solid #e2e8f0;
}
.expense-summary h2 {
margin-top: 0;
margin-bottom: 1rem;
color: #1e293b;
}
.summary-row {
display: flex;
justify-content: space-between;
padding: 0.5rem 0;
border-bottom: 1px solid #f1f5f9;
}
.summary-row.remaining {
border-bottom: none;
font-weight: 600;
padding-top: 0.75rem;
border-top: 2px solid #e2e8f0;
margin-top: 0.25rem;
}
.summary-value {
font-weight: 600;
}
.expenses-total {
color: #dc2626;
}
.negative {
color: #dc2626;
}
/* Category Breakdown */
.category-breakdown {
margin-top: 1rem;
padding-top: 0.75rem;
border-top: 1px solid #e2e8f0;
}
.category-breakdown h3 {
margin: 0 0 0.5rem 0;
font-size: 0.9rem;
color: #64748b;
}
.category-breakdown ul {
list-style: none;
padding: 0;
margin: 0;
}
.category-breakdown li {
display: flex;
justify-content: space-between;
padding: 0.35rem 0.5rem;
margin-bottom: 0.25rem;
border-radius: 4px;
font-size: 0.9rem;
}
/* Category badge colors */
.badge-food { background: #fef3c7; color: #92400e; }
.badge-transport { background: #dbeafe; color: #1e40af; }
.badge-entertainment { background: #ede9fe; color: #5b21b6; }
.badge-other { background: #f1f5f9; color: #475569; }
/* Expense List */
.expense-list h2 {
color: #1e293b;
}
.expense-list ul {
list-style: none;
padding: 0;
}
.expense-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
margin-bottom: 0.5rem;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 6px;
}
.expense-details {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.expense-description {
font-weight: 500;
color: #1e293b;
}
.expense-category {
display: inline-block;
font-size: 0.75rem;
padding: 0.15rem 0.5rem;
border-radius: 12px;
font-weight: 500;
width: fit-content;
}
.expense-actions {
display: flex;
align-items: center;
gap: 0.75rem;
}
.expense-amount {
font-weight: 600;
color: #1e293b;
}
.delete-btn {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: #fee2e2;
color: #dc2626;
border: none;
border-radius: 50%;
cursor: pointer;
font-size: 1.1rem;
line-height: 1;
}
.delete-btn:hover {
background: #fecaca;
}
.empty-state {
text-align: center;
color: #94a3b8;
font-style: italic;
padding: 2rem 0;
}
What do these styles do?
- The .app-header gives ExpenseFlow a bold purple header bar with the brand name.
- The .balance-card creates a subtle input section for the starting balance.
- The .expense-summary uses row layout with a divider between the totals and the remaining balance.
- Category badges (.badge-food, .badge-transport) get distinct background colors so each category is easy to identify at a glance.
- The .delete-btn is a circular red button matching the UI mockup's trash-icon style.
- Save App.css and check your browser.
- You should see the app centered on the page with a styled form card, a green summary section showing Total: $0.00, and the empty-state message in italic gray text.
Styles not showing up?
- Make sure your App.jsx still has import './App.css' at the top. Without that import, Vite doesn't bundle the styles.
- Check that className values in your JSX match the CSS selectors exactly. For example, className="expense-summary" must match .expense-summary in the CSS.
Why aren't my CSS styles applying to my React components?
✔️ Awesome, I've got everything!
Great. Double check both files are saved.
ⓧ I'd like to double check the full code
body {
margin: 0;
padding: 0;
background: #ffffff;
}
/* App layout */
.app {
max-width: 600px;
margin: 0 auto;
padding: 2rem 1rem;
font-family: system-ui, -apple-system, sans-serif;
}
.app-header {
background: #4f46e5;
color: white;
padding: 1rem 1.5rem;
border-radius: 8px;
margin-bottom: 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.app-header h1 { margin: 0; font-size: 1.5rem; }
.balance-card {
background: #f1f5f9;
padding: 1.25rem 1.5rem;
border-radius: 8px;
margin-bottom: 1.5rem;
border: 1px solid #e2e8f0;
}
.balance-card label { display: block; margin-bottom: 0.5rem; font-weight: 600; color: #334155; }
.balance-card input { width: 100%; padding: 0.5rem; border: 1px solid #cbd5e1; border-radius: 4px; font-size: 1rem; box-sizing: border-box; }
.container { display: flex; flex-direction: column; gap: 1.5rem; }
.expense-form { background: #f8fafc; padding: 1.5rem; border-radius: 8px; border: 1px solid #e2e8f0; }
.expense-form h2 { margin-top: 0; margin-bottom: 1rem; color: #1e293b; }
.form-group { margin-bottom: 1rem; }
.form-group label { display: block; margin-bottom: 0.25rem; font-weight: 500; color: #475569; }
.form-group input, .form-group select { width: 100%; padding: 0.5rem; border: 1px solid #cbd5e1; border-radius: 4px; font-size: 1rem; box-sizing: border-box; }
.expense-form button { width: 100%; padding: 0.75rem; background: #4f46e5; color: white; border: none; border-radius: 6px; font-size: 1rem; font-weight: 500; cursor: pointer; }
.expense-form button:hover { background: #4338ca; }
.expense-summary { background: #f8fafc; padding: 1.5rem; border-radius: 8px; border: 1px solid #e2e8f0; }
.expense-summary h2 { margin-top: 0; margin-bottom: 1rem; color: #1e293b; }
.summary-row { display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid #f1f5f9; }
.summary-row.remaining { border-bottom: none; font-weight: 600; padding-top: 0.75rem; border-top: 2px solid #e2e8f0; margin-top: 0.25rem; }
.summary-value { font-weight: 600; }
.expenses-total { color: #dc2626; }
.negative { color: #dc2626; }
.category-breakdown { margin-top: 1rem; padding-top: 0.75rem; border-top: 1px solid #e2e8f0; }
.category-breakdown h3 { margin: 0 0 0.5rem 0; font-size: 0.9rem; color: #64748b; }
.category-breakdown ul { list-style: none; padding: 0; margin: 0; }
.category-breakdown li { display: flex; justify-content: space-between; padding: 0.35rem 0.5rem; margin-bottom: 0.25rem; border-radius: 4px; font-size: 0.9rem; }
.badge-food { background: #fef3c7; color: #92400e; }
.badge-transport { background: #dbeafe; color: #1e40af; }
.badge-entertainment { background: #ede9fe; color: #5b21b6; }
.badge-other { background: #f1f5f9; color: #475569; }
.expense-list h2 { color: #1e293b; }
.expense-list ul { list-style: none; padding: 0; }
.expense-item { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 1rem; margin-bottom: 0.5rem; background: #fff; border: 1px solid #e2e8f0; border-radius: 6px; }
.expense-details { display: flex; flex-direction: column; gap: 0.25rem; }
.expense-description { font-weight: 500; color: #1e293b; }
.expense-category { display: inline-block; font-size: 0.75rem; padding: 0.15rem 0.5rem; border-radius: 12px; font-weight: 500; width: fit-content; }
.expense-actions { display: flex; align-items: center; gap: 0.75rem; }
.expense-amount { font-weight: 600; color: #1e293b; }
.delete-btn { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; background: #fee2e2; color: #dc2626; border: none; border-radius: 50%; cursor: pointer; font-size: 1.1rem; line-height: 1; }
.delete-btn:hover { background: #fecaca; }
.empty-state { text-align: center; color: #94a3b8; font-style: italic; padding: 2rem 0; }
Now test the full flow. Add a few expenses across different categories and watch the summary update in real time.
- Enter 100 in the Starting Balance field.
- Add an expense with description Lunch, amount 12.50, and category Food.
- Add another with description Bus ticket, amount 3.00, and category Transport.
- The summary should show Starting Balance $100.00, Total Expenses -$15.50, and Remaining Balance $84.50 with a per-category breakdown.
- Delete one expense and confirm the summary updates immediately.
Your ExpenseFlow app is fully functional and styled. You've built three components that communicate through props, manage state with hooks, and render dynamic lists with real-time balance tracking.
Secret mission
Add Category Filtering and Date Range
Your expense tracker works great, but real budgeting means slicing data by category and time period. Can you add a filter bar that narrows down both the list and the summary in real time?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The Vite dev server running at http://localhost:5173.
- The expense-tracker project folder on your Desktop.
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with your expense tracker.
- Your dev server stays running and you can keep adding features.
- Your expense-tracker folder remains on your Desktop for easy access.
Pause - I'll come back to this later
Shut down the running dev server to free up your terminal. Your project files stay on disk so you can pick up where you left off.
- Switch to the terminal where your dev server is running.
- Press Ctrl+C to stop the server.
- When you come back, navigate to the expense-tracker folder and run npm run dev to start up again.
Delete - I don't want to use this again
Remove all project files and start fresh. This permanently deletes your expense tracker code.
- Switch to the terminal where your dev server is running.
- Press Ctrl+C to stop the server.
- Navigate to your Desktop and delete the project folder by running these commands:
macOS
cd ~/Desktop
rm -rf expense-tracker
Windows
cd ~/Desktop
rmdir /s /q expense-tracker
- Confirm the folder is gone by running ls and checking that expense-tracker no longer appears.
Nice Work!
Nice Work!
Nice work! You just built ExpenseFlow, a fully functional expense tracker from scratch using React and Vite.
You've learned how to:
- Built reusable components (ExpenseForm, ExpenseList, ExpenseSummary) that communicate through props.
- Managed application state with useState and implemented controlled forms with validation, list rendering with .map(), and conditional rendering.
- Used .reduce() to compute derived data like spending totals and per-category breakdowns.
- Secret Mission: Added multi-criteria filtering with category and date range using coordinated filter state.
Ready to quiz yourself?