Let’s apply the ideas of the chapter to one realistic task: summarise a list of orders.
const orders = [
{ id: 1, customer: "Maya", items: [{ name: "Book", price: 12, qty: 2 }], status: "paid" },
{ id: 2, customer: "Arjun", items: [{ name: "Pen", price: 2, qty: 10 }, { name: "Notebook", price: 4, qty: 1 }], status: "pending" },
{ id: 3, customer: "Maya", items: [{ name: "Bag", price: 35, qty: 1 }], status: "paid" }
];
// small, pure building blocks
const lineTotal = item => item.price * item.qty;
const orderTotal = order => order.items.map(lineTotal).reduce((a, b) => a + b, 0);
const isPaid = order => order.status === "paid";
const sum = numbers => numbers.reduce((a, b) => a + b, 0);
// compose them
const paidRevenue = orders => sum(orders.filter(isPaid).map(orderTotal));
console.log(paidRevenue(orders)); // → 59
Each helper does one thing and can be tested alone: lineTotal({ price: 12, qty: 2 }) is 24.
Grouping with reduce
const orders = [
{ customer: "Maya", total: 24 },
{ customer: "Arjun", total: 24 },
{ customer: "Maya", total: 35 }
];
const totalsByCustomer = orders.reduce(
(totals, order) => ({ ...totals, [order.customer]: (totals[order.customer] ?? 0) + order.total }),
{}
);
console.log(totalsByCustomer); // → { Maya: 59, Arjun: 24 }
No variable was changed, and each step produced a new object.
Immutable update of one order
const orders = [
{ id: 1, status: "pending" },
{ id: 2, status: "pending" }
];
const markPaid = (list, id) =>
list.map(order => (order.id === id ? { ...order, status: "paid" } : order));
const updated = markPaid(orders, 2);
console.log(orders[1].status); // → pending (original untouched)
console.log(updated[1].status); // → paid
Sorting without surprises
const byTotalDesc = (a, b) => b.total - a.total;
const orders = [{ total: 24 }, { total: 59 }, { total: 35 }];
const ranked = [...orders].sort(byTotalDesc); // copy first: sort mutates
console.log(ranked.map(o => o.total)); // → [59, 35, 24]
console.log(orders.map(o => o.total)); // → [24, 59, 35]
Putting the page on top
The calculations are pure. The page updating is the side effect, kept in one small place.
<ul id="report"></ul>
<script>
const sales = [{ product: "Book", amount: 24 }, { product: "Pen", amount: 20 }, { product: "Book", amount: 12 }];
// pure: build the data
const totals = sales.reduce((acc, s) => ({ ...acc, [s.product]: (acc[s.product] ?? 0) + s.amount }), {});
const lines = Object.entries(totals).map(([product, total]) => `${product}: $${total}`);
// impure: render
document.querySelector("#report").replaceChildren(
...lines.map(text => Object.assign(document.createElement("li"), { textContent: text }))
);
</script>
What you gained
- Each function is small, named and testable.
- Data flows in one direction through the pipeline.
- Nothing is changed behind your back.
When to use a loop instead
If a for...of loop makes a task clearer (especially with break, await or several outputs at once), use it. Functional style is a way of thinking about data flow, not a rule against loops.
Exercise: average rating
Given an array of reviews { product, rating }, write a pure function that returns the average rating for a given product, or 0 if there are no reviews.
Show a possible solution
const averageRating = (reviews, product) => {
const ratings = reviews.filter(r => r.product === product).map(r => r.rating);
return ratings.length === 0 ? 0 : ratings.reduce((a, b) => a + b, 0) / ratings.length;
};
const reviews = [{ product: "Pen", rating: 4 }, { product: "Pen", rating: 5 }, { product: "Book", rating: 3 }];
console.log(averageRating(reviews, "Pen")); // → 4.5
console.log(averageRating(reviews, "Lamp")); // → 0