In short
Where did this month's pocket money go? An expense tracker answers that, and building one makes you practise arrays, reduce, forms, dates and localStorage all at once.
An app for logging income/expenses with running totals and category breakdowns — practice for array `.reduce()`, form handling and simple data visualization. It is a practical exercise in modeling records, deriving aggregate values, validating input, persisting state, and presenting data in a useful visual summary.
Think of it like this
Mummy's hisaab diary
Date, item, amount, and a total at the bottom of every page. Some lines are money in (pocket money), most are money out (Zomato, again). Your app is that diary, except it does the adding up for you and never loses a page.
Watch it happen
reduce adds up the totals (in paise)
1/5reduce starts with { income: 0, expense: 0 } and looks at the first transaction.
The big ideas
What you need to know
One array of transactions
Each one has an id, title, amount, type (income or expense), category and date.
Calculate totals, don't store them
Work out income, expense and balance with reduce every time you render. Stored totals drift out of sync.
Store paise, not rupees
Keep amounts as whole numbers in paise to dodge floating point errors. Divide by 100 only for display.
Filter by month and category
filter on date and category lets users answer "how much on food in March?".
How it works
Build plan
- 1
Form and list
Inputs for title, amount, type and category, plus a list below.
- 2
Add and delete
Push new transactions, remove by id, and re-render.
- 3
Totals
Income, expense and balance cards calculated with reduce.
- 4
Persist
Save the array to localStorage and load it on startup.
- 5
Filters
Month and category filters, then an optional chart.
See it in code
Walk through the code
1const transactions = [2 { id: crypto.randomUUID(), title: "Pocket money", amount: 500000, type: "income" },3 { id: crypto.randomUUID(), title: "Mess bill", amount: 320000, type: "expense" },4 { id: crypto.randomUUID(), title: "Movie", amount: 45050, type: "expense" },5]; // amounts in paise to avoid decimal errors6const totals = transactions.reduce(7 (acc, t) => {8 acc[t.type] += t.amount;9 return acc;10 },11 { income: 0, expense: 0 }12);13 14const balance = (totals.income - totals.expense) / 100;15console.log("₹" + balance.toFixed(2)); // "₹1349.50"1/3Each transaction has an id, title, amount in paise and type.
reduce starts with { income: 0, expense: 0 } and adds each amount to the matching bucket. One pass gives you both totals.
Save yourself the pain
Common mistakes
Oops
Adding input.value straight into your totals.
Do this instead
Input values are strings, so "200" + "300" becomes "200300". Convert with Number() and validate it's above 0.
Oops
Storing the balance as its own variable and updating it by hand.
Do this instead
Calculate it from the transactions every render. One source of truth, no mismatches.
Your mission
Milestone 1: add and total
Quick check
What does this return? [{ amount: "200" }, { amount: "300" }].reduce((s, t) => s + t.amount, 0)
Go deeper
- 1Use `.reduce()` to calculate running totals and category sums. Verify the behavior with realistic data and handle the failure or empty state as well.
- 2Persist entries to localStorage so the tracker survives a refresh. Validate your understanding by applying it in a small practical example.
- 3Add a simple bar or pie breakdown by category, even with basic CSS/SVG.
More projects at this stop
Stuck on this topic? Ask an Insider
Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.