JavaScript Tutorial

JavaScript Project: Expense Tracker

Add amounts with a label, list them, compute the total, and keep the list in localStorage.

What you will build

An expense list. Each row has a label and an amount. The page totals every amount with reduce. Reload the preview and the rows are still there: the array is saved as JSON in localStorage.

This is the to-do pattern plus money and persistence. The DOM is a redraw. Delete filters by id. The total is never typed by hand.

Open an example with Try it in JavaScript. That loads /javascript/try — a live page and a console.

Methods you will use

MethodJob on this page
arrayHolds each expense as { id, label, amount }
reduceSums every amount into one total
localStorageSaves the JSON list so a reload does not wipe it
JSON.parse / JSON.stringifyTurns the array into a string and back
Number + toFixedValidates the amount and formats the total

Save after every change that mutates the array: add and delete. Render after save. If you render first and forget to save, the next reload shows the old list.

Build in slices

Two fields, an add button, a list, and a total line. Labels on both inputs.

Example

<label for="label">Label</label>
<input id="label" type="text" placeholder="Bus ticket">
<label for="amount">Amount</label>
<input id="amount" type="number" min="0" step="0.01">
<button id="add" type="button">Add expense</button>
<ul id="list"></ul>
<p id="total">Total: $0.00</p>

Load JSON carefully. Bad storage should become an empty array, not a thrown error that kills the page.reduce starts at 0 so an empty list totals to zero.

Example

const KEY = "expenses";

function loadItems() {
  try {
    const raw = localStorage.getItem(KEY);
    const data = raw ? JSON.parse(raw) : [];
    if (!Array.isArray(data)) return [];
    return data;
  } catch (err) {
    return [];
  }
}

function totalOf(items) {
  return items.reduce(function (sum, item) {
    return sum + item.amount;
  }, 0);
}

function saveItems(items) {
  localStorage.setItem(KEY, JSON.stringify(items));
}

Add only when the label is non-empty and the amount is a finite number greater than zero. Storeamount as a number, not as a formatted string, so reduce can add it.

Example

<p id="total"></p>
<script>
  const items = [
    { id: 1, label: "Bus ticket", amount: 2.75 },
    { id: 2, label: "Notebook", amount: 4.20 }
  ];

  function totalOf(rows) {
    return rows.reduce(function (sum, item) {
      return sum + item.amount;
    }, 0);
  }

  document.getElementById("total").textContent = "Total: $" + totalOf(items).toFixed(2);
</script>

Add two rows, reload /javascript/try, and confirm both remain. Then delete one. The total must drop. Use Try it in JavaScript.

Complete document

This file is the tracker you would keep. Delete is per row. The total uses reduce on every render. Storage key is expenses.

Example

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Expense Tracker</title>
  <style>
    body {
      margin: 0;
      min-height: 100vh;
      display: grid;
      place-items: center;
      background: #fefce8;
      font-family: "Segoe UI", system-ui, sans-serif;
      color: #1c1917;
    }
    .card {
      width: min(26rem, 92vw);
      background: #fffbeb;
      border: 1px solid #fde68a;
      padding: 1.4rem 1.5rem 1.5rem;
    }
    h1 { margin: 0 0 0.85rem; font-size: 1.4rem; }
    label { display: block; font-weight: 700; margin: 0.45rem 0 0.3rem; }
    input {
      width: 100%;
      box-sizing: border-box;
      padding: 0.45rem 0.55rem;
      border: 1px solid #d6d3d1;
      font: inherit;
    }
    button {
      margin-top: 0.75rem;
      padding: 0.5rem 0.85rem;
      border: 0;
      background: #854d0e;
      color: #fffbeb;
      font: inherit;
      font-weight: 700;
      cursor: pointer;
    }
    #note { margin: 0.55rem 0 0; min-height: 1.2rem; color: #78716c; }
    ul { list-style: none; margin: 1rem 0 0; padding: 0; }
    li {
      display: flex;
      gap: 0.5rem;
      align-items: center;
      padding: 0.45rem 0;
      border-top: 1px solid #fde68a;
    }
    li span { flex: 1; }
    li strong { font-variant-numeric: tabular-nums; }
    li button {
      margin: 0;
      background: #d6d3d1;
      color: #1c1917;
      font-weight: 600;
    }
    #total {
      margin: 0.85rem 0 0;
      padding-top: 0.75rem;
      border-top: 2px solid #fde68a;
      font-weight: 800;
    }
  </style>
</head>
<body>
  <article class="card">
    <h1>Expense Tracker</h1>
    <label for="label">Label</label>
    <input id="label" type="text" placeholder="Bus ticket">
    <label for="amount">Amount</label>
    <input id="amount" type="number" min="0" step="0.01" placeholder="2.75">
    <button id="add" type="button">Add expense</button>
    <p id="note"></p>
    <ul id="list"></ul>
    <p id="total"></p>
  </article>
  <script>
    const KEY = "expenses";
    const labelField = document.getElementById("label");
    const amountField = document.getElementById("amount");
    const list = document.getElementById("list");
    const totalEl = document.getElementById("total");
    const note = document.getElementById("note");

    function loadItems() {
      try {
        const raw = localStorage.getItem(KEY);
        const data = raw ? JSON.parse(raw) : [];
        return Array.isArray(data) ? data : [];
      } catch (err) {
        return [];
      }
    }

    let items = loadItems();
    let nextId = items.reduce(function (max, item) {
      return item.id > max ? item.id : max;
    }, 0) + 1;

    function money(n) {
      return "$" + n.toFixed(2);
    }

    function totalOf(rows) {
      return rows.reduce(function (sum, item) {
        return sum + Number(item.amount);
      }, 0);
    }

    function save() {
      localStorage.setItem(KEY, JSON.stringify(items));
    }

    function render() {
      list.replaceChildren();
      items.forEach(function (item) {
        const row = document.createElement("li");
        const name = document.createElement("span");
        name.textContent = item.label;
        const cost = document.createElement("strong");
        cost.textContent = money(Number(item.amount));
        const del = document.createElement("button");
        del.type = "button";
        del.textContent = "Delete";
        del.addEventListener("click", function () {
          items = items.filter(function (rowItem) {
            return rowItem.id !== item.id;
          });
          save();
          render();
        });
        row.append(name, cost, del);
        list.appendChild(row);
      });
      totalEl.textContent = "Total: " + money(totalOf(items));
    }

    document.getElementById("add").addEventListener("click", function () {
      const label = labelField.value.trim();
      const amount = Number(amountField.value);
      if (!label) {
        note.textContent = "Enter a label.";
        return;
      }
      if (!isFinite(amount) || amount <= 0) {
        note.textContent = "Enter an amount greater than 0.";
        return;
      }
      items.push({ id: nextId, label: label, amount: amount });
      nextId += 1;
      labelField.value = "";
      amountField.value = "";
      note.textContent = "";
      save();
      render();
      labelField.focus();
    });

    render();
  </script>
</body>
</html>

JSON in localStorage

JSON.stringify will drop functions and turn NaN into null. That is why the amount is validated before it enters the array. On the way back, Number(item.amount)in reduce protects against an old string that snuck into storage.

Date.now() can be an id if you add at human speed. Two adds in the same millisecond would collide. The complete document uses an incrementing nextId seeded from the largest stored id so reloads do not reuse 1.

Common mistakes

  • Saving the formatted string "$2.75". reduce would concatenate or yield NaN.
  • Calling JSON.parse on null without a guard. Missing storage is not invalid JSON, but a leftover "" is.
  • Totalling in a running variable and forgetting to subtract on delete. Recalculate with reduce every render.
  • Using innerHTML to print the label. Treat labels as text.
  • Storing the list and never calling save after delete. Reload would resurrect the row.

Practice tasks

  1. Add a Clear all button that sets the array to [], saves, and rerenders.
  2. Show a count of rows next to the total, using items.length.
  3. Ignore amounts with more than two decimal places, or round them with Math.round(amount * 100) / 100. Preview at /javascript/try.

FAQ: JavaScript Project: Expense Tracker

Common questions about this page.

What is the StudyGrid JavaScript tutorial?

The StudyGrid JavaScript tutorial is a full beginner track: variables, functions, arrays, if/else, the DOM, events, storage, and fetch. Each chapter has copy-and-run examples.

Should I run javascript expense project examples locally for better learning?

Yes. Use the browser editor on StudyGrid for a quick check, then Download the example and run it on your computer. Local runs show real errors and the real toolchain, which is one of the fastest ways to learn javascript expense project in this JavaScript JavaScript lesson (JavaScript Project: Expense Tracker).

Is the JavaScript editor the same as Try Python?

No. Try JavaScript is a live page plus a console at /javascript/try. Try Python stays at /try. JavaScript lessons never open the Python editor.

Do I need to install anything to learn JavaScript?

No. Open a chapter, click Try it in JavaScript, and the page and console update in the browser.

Where should I start the JavaScript tutorial?

Start at JavaScript Intro, then Where To, Output, and Syntax. After variables and functions, continue to the DOM and events. Use Next at the bottom of each chapter.

Is the JavaScript tutorial free?

Yes. The JavaScript studio on StudyGrid (studygrid.in) is free: dashboard, chapters, and the live editor.