Skip to content
WANCODE
BM

Monthly Expense Tracker

1.8 hIntermediateJavaScriptArrays & objectsreduceFormslocalStorage

Outcome

A budgeting app with a live summary (spent, balance that turns red when you overspend), category totals with bars, a deletable expense list, and data that survives a refresh.

Learning objectives

  • Model data as an array of objects with the right types (amounts as numbers, not strings)
  • Derive totals with reduce and group them into an object keyed by category
  • Validate form input and show inline error messages
  • Persist and restore data with localStorage and JSON

Prerequisites

  • Comfortable with arrays, objects, functions and DOM events — the JavaScript track covers these
  • Recommended: finish the To-do App That Remembers project first — this one uses the same state → render → save pattern

Expected behaviour

  • Adding “Nasi lemak · RM 8.50 · Food” puts it in the list and raises Spent and the Food total by RM 8.50.
  • An empty description or an amount of 0 / negative shows an error and adds nothing.
  • Deleting an expense removes it and every total updates immediately.
  • Balance = Budget − Spent, and it turns red when it goes below zero.
  • Refreshing the page keeps every expense.

Budgeting apps look complicated, but underneath they're one array of expenses and a handful of numbers calculated from that array. You never store the total — you work it out from the data every time. That keeps everything in sync for free.

JavaScript
expenses = [
  { id: "k1", desc: "Nasi lemak", amount: 8.5,  category: "Food" },
  { id: "k2", desc: "LRT",        amount: 3.2,  category: "Transport" },
  { id: "k3", desc: "Teh tarik",  amount: 2.8,  category: "Food" },
]

total       → 14.5
byCategory  → { Food: 11.3, Transport: 3.2, Bills: 0, … }

Stored data (top) and derived data (bottom). Only the top part is saved.

reduce folds a whole array into one value — perfect for totals.

Computers store decimals in binary, so 0.1 + 0.2 is 0.30000000000000004. For a personal tracker, rounding on display with toFixed(2) is fine. Real financial systems usually store whole sen (integers) instead.

Milestones

Handle the form's submit event:

1. preventDefault(), then read the description (trim() it), the amount (Number(...)) and the category. 2. If the description is empty or the amount isn't a number above 0, put a message in #form-error and stop. 3. Otherwise clear #form-error, push { id, desc, amount, category } into expenses, call render() and form.reset().

In render(), draw one <li> per expense in #expense-list showing the description, the category and the amount via formatRM().

Checks

  • Adding shows the expense in the list
  • Amounts are stored as numbers
  • Invalid input shows an error and adds nothing
  • The form resets and the error clears after a valid add
Completion checklist

Tick these when they're true for your project. The project completes when every milestone passes and every item is ticked.

Preview

Console output from your code appears here.

Step 1 of 6