Build a React Budget Tracker App – Learn React & Context API with this Fun Project
In this hands-on project, we‘re going to build an expense tracking app using React. It will allow users to set a budget, add & remove expenses, and calculate totals. By the end, you‘ll have a solid grasp of key React concepts including:
- Breaking down a UI into components
- Managing state with the Context API
- Updating state with actions & reducers
Here‘s a preview of the final app:
Let‘s dive in and start building it out, step-by-step!
Setting Up the Project
First, make sure you have Node.js installed. Next, open up your terminal and navigate to the directory where you keep your projects. Run this command to generate a new React project:
npx create-react-app budget-tracker
Once that finishes, navigate into the new directory it created:
cd budget-tracker
We‘re going to use a few additional packages in this project:
- Bootstrap for quick & easy styling
- UUID for generating unique IDs
- React Icons for including icons
Install them by running:
npm install bootstrap uuid react-icons
Start up the development server with:
npm start
You should see the default Create React App page open up in your browser at http://localhost:3000
. We‘re ready to start coding!
Breaking Down the UI
Let‘s think about how to divide our app‘s UI into distinct React components.
At the top we have a header section with:
- The total budget amount
- Total expenses so far
- Remaining budget
Then we have the main section which shows:
- A list of expenses (name & cost)
- A form to add new expenses
So we‘ll create components for:
Budget
Remaining
ExpenseTotal
ExpenseList
ExpenseItem
AddExpenseForm
And an App
component will act as the main container.
Creating the Basic Components
Let‘s start by generating the basic component files. In your src
directory, create a new folder called components
.
Inside src/components
, create these 6 files:
Budget.js
Remaining.js
ExpenseTotal.js
ExpenseList.js
ExpenseItem.js
AddExpenseForm.js
Open up each one and add the following starter code:
import React from ‘react‘;
const MyComponent = () => {
return (
<div>
<h2>MyComponent</h2>
</div>
);
};
export default MyComponent;
Make sure to change MyComponent
to the actual component name in each file.
Setting Up the Context
Before we flesh out the individual components, let‘s set up the Context API to manage our app‘s global state.
Create a new file called AppContext.js
inside a context
folder in src
. This is where we‘ll initialize the context and set up the provider.
// src/context/AppContext.js
import React, { createContext, useReducer } from ‘react‘;
// Create the initial state
const initialState = {
budget: 2000,
expenses: [
{ id: 12, name: ‘shopping‘, cost: 40 },
{ id: 13, name: ‘holiday‘, cost: 400 },
{ id: 14, name: ‘car service‘, cost: 50 },
],
};
// Create the context
export const AppContext = createContext();
// Create the provider component
export const AppProvider = (props) => {
// Set up the reducer
const [state, dispatch] = useReducer(AppReducer, initialState);
return (
<AppContext.Provider
value={{
budget: state.budget,
expenses: state.expenses,
dispatch,
}}
>
{props.children}
</AppContext.Provider>
);
};
Now any child component wrapped in AppProvider
can access budget
, expenses
and dispatch
.
We still need to create the AppReducer
to handle updating state. Add this above the initialState
:
// The reducer function
const AppReducer = (state, action) => {
switch (action.type) {
default:
return state;
}
};
For now it doesn‘t do much, but we‘ll add to it as we build out the actions.
Fleshing Out the Components
Now that we have our state management set up, let‘s circle back to the components.
Budget
This will display the total budget. Update Budget.js
:
import React, { useContext } from ‘react‘;
import { AppContext } from ‘../context/AppContext‘;
const Budget = () => {
const { budget } = useContext(AppContext);
return (
<div className=‘alert alert-secondary‘>
<span>Budget: £{budget}</span>
</div>
);
};
export default Budget;
Key things:
- Import
useContext
andAppContext
- Use them to pull the
budget
value from state - Display it in the returned JSX
Remaining
This is very similar, but it will calculate and display the remaining budget.
import React, { useContext } from ‘react‘;
import { AppContext } from ‘../context/AppContext‘;
const Remaining = () => {
const { expenses, budget } = useContext(AppContext);
const totalExpenses = expenses.reduce((total, item) => {
return (total += item.cost);
}, 0);
const alertType = totalExpenses > budget ? ‘alert-danger‘ : ‘alert-success‘;
return (
<div className={`alert ${alertType}`}>
<span>Remaining: £{budget - totalExpenses}</span>
</div>
);
};
export default Remaining;
Key things:
- Pull both
expenses
andbudget
from state - Calculate total expenses by using
reduce
to sum up all theitem.cost
values - Determine if expenses exceed budget and set the Bootstrap alert type accordingly
- Display the result of
budget - totalExpenses
ExpenseTotal
Again very similar, but showing the total expenses:
import React, { useContext } from ‘react‘;
import { AppContext } from ‘../context/AppContext‘;
const ExpenseTotal = () => {
const { expenses } = useContext(AppContext);
const totalExpenses = expenses.reduce((total, item) => {
return (total += item.cost);
}, 0);
return (
<div className=‘alert alert-primary‘>
<span>Spent so far: £{totalExpenses}</span>
</div>
);
};
export default ExpenseTotal;
ExpenseList & ExpenseItem
The ExpenseList
component will render a list of ExpenseItem
components by mapping over the expenses
array.
// src/components/ExpenseList.js
import React, { useContext } from ‘react‘;
import ExpenseItem from ‘./ExpenseItem‘;
import { AppContext } from ‘../context/AppContext‘;
const ExpenseList = () => {
const { expenses } = useContext(AppContext);
return (
<ul className=‘list-group‘>
{expenses.map((expense) => (
<ExpenseItem id={expense.id} name={expense.name} cost={expense.cost} />
))}
</ul>
);
};
export default ExpenseList;
// src/components/ExpenseItem.js
import React from ‘react‘;
const ExpenseItem = (props) => {
return (
<li className=‘list-group-item d-flex justify-content-between align-items-center‘>
{props.name}
<div>
<span className=‘badge badge-primary badge-pill mr-3‘>£{props.cost}</span>
</div>
</li>
);
};
export default ExpenseItem;
AddExpenseForm
The final component is the form that lets users add new expenses.
// src/components/AddExpenseForm.js
import React, { useContext, useState } from ‘react‘;
import { AppContext } from ‘../context/AppContext‘;
import { v4 as uuidv4 } from ‘uuid‘;
const AddExpenseForm = () => {
const { dispatch } = useContext(AppContext);
const [name, setName] = useState(‘‘);
const [cost, setCost] = useState(‘‘);
const onSubmit = (event) => {
event.preventDefault();
const expense = {
id: uuidv4(),
name,
cost: parseInt(cost),
};
dispatch({
type: ‘ADD_EXPENSE‘,
payload: expense,
});
};
return (
<form onSubmit={onSubmit}>
<div className=‘row‘>
<div className=‘col-sm‘>
<label for=‘name‘>Name</label>
<input
required=‘required‘
type=‘text‘
className=‘form-control‘
id=‘name‘
value={name}
onChange={(event) => setName(event.target.value)}
></input>
</div>
<div className=‘col-sm‘>
<label for=‘cost‘>Cost</label>
<input
required=‘required‘
type=‘number‘
className=‘form-control‘
id=‘cost‘
value={cost}
onChange={(event) => setCost(event.target.value)}
></input>
</div>
</div>
<div className=‘row‘>
<div className=‘col-sm‘>
<button type=‘submit‘ className=‘btn btn-primary mt-3‘>
Save
</button>
</div>
</div>
</form>
);
};
export default AddExpenseForm;
Key things:
- Import
useState
to track form input state locally - When the form is submitted:
- Prevent the default browser refresh
- Create an expense object with name, cost, and a unique ID from uuid
- Dispatch an
ADD_EXPENSE
action with the expense as payload
But we still need to handle that action type in our reducer! Open up AppContext.js
and add this case to the switch statement:
case ‘ADD_EXPENSE‘:
return {
...state,
expenses: [...state.expenses, action.payload],
};
Now when that action is dispatched, the reducer will update the expenses
array by returning a new state object with the payload expense included.
Bringing It All Together
We‘ve built all the pieces, now we just need to put them together in App.js
. Update it like so:
import React from ‘react‘;
import ‘bootstrap/dist/css/bootstrap.min.css‘;
import Budget from ‘./components/Budget‘;
import Remaining from ‘./components/Remaining‘;
import ExpenseTotal from ‘./components/ExpenseTotal‘;
import ExpenseList from ‘./components/ExpenseList‘;
import AddExpenseForm from ‘./components/AddExpenseForm‘;
import { AppProvider } from ‘./context/AppContext‘;
const App = () => {
return (
<AppProvider>
<div className=‘container‘>
<h1 className=‘mt-3‘>My Budget Planner</h1>
<div className=‘row mt-3‘>
<div className=‘col-sm‘>
<Budget />
</div>
<div className=‘col-sm‘>
<Remaining />
</div>
<div className=‘col-sm‘>
<ExpenseTotal />
</div>
</div>
<h3 className=‘mt-3‘>Expenses</h3>
<div className=‘row ‘>
<div className=‘col-sm‘>
<ExpenseList />
</div>
</div>
<h3 className=‘mt-3‘>Add Expense</h3>
<div className=‘row mt-3‘>
<div className=‘col-sm‘>
<AddExpenseForm />
</div>
</div>
</div>
</AppProvider>
);
};
export default App;
The key part is wrapping everything in the AppProvider
component. This allows all the children to access the budget context.
Adding a Remove Expense Feature
Let‘s add one more feature before we wrap up. It would be useful to be able to remove an expense.
First update the ExpenseItem
component to include a delete icon:
import React, { useContext } from ‘react‘;
import { TiDelete } from ‘react-icons/ti‘;
import { AppContext } from ‘../context/AppContext‘;
const ExpenseItem = (props) => {
const { dispatch } = useContext(AppContext);
const handleDeleteExpense = () => {
dispatch({
type: ‘DELETE_EXPENSE‘,
payload: props.id,
});
};
return (
<li className=‘list-group-item d-flex justify-content-between align-items-center‘>
{props.name}
<div>
<span className=‘badge badge-primary badge-pill mr-3‘>£{props.cost}</span>
<TiDelete size=‘1.5em‘ onClick={handleDeleteExpense}></TiDelete>
</div>
</li>
);
};
export default ExpenseItem;
When the delete icon is clicked, it will dispatch a DELETE_EXPENSE
action with the expense id as the payload.
Now we need to handle that action in the reducer:
case ‘DELETE_EXPENSE‘:
return {
...state,
expenses: state.expenses.filter((expense) => expense.id !== action.payload),
};
This filters the expenses
array, removing the expense with the matching id
.
And with that, we have a fully functional React Budget Tracker app!
Next Steps & Challenges
Want to take this further? Here are a couple challenges to test your new React skills:
- Add the ability to edit the budget
- Implement a search/filter feature for expenses
Thank you for following along with this tutorial! Remember, you can find the full source code here.
Feel free to reach out if you have any questions. Now go build something cool with React!