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:

Budget Tracker App Screenshot

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 and AppContext
  • 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 and budget from state
  • Calculate total expenses by using reduce to sum up all the item.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:

  1. Add the ability to edit the budget
  2. 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!

Similar Posts