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
reduceand group them into an object keyed by category - ▸Validate form input and show inline error messages
- ▸Persist and restore data with
localStorageand 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.
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
Console output from your code appears here.