DataLens

Array methods (map, filter, reduce, forEach)

Transform data declaratively with map, filter, reduce, find, some, every and sort, chain them into readable pipelines, and avoid the classic sort and mutation traps.

Level: Intermediate · 10 min read

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

The idea

Loops can do anything, but they make you read every line to find out what they do. Array methods name the intent: filter keeps some items, map transforms each item, reduce combines them into one value. A chain like products.filter(inStock).map(toCard) reads like a sentence. This is the everyday style of modern JavaScript, and the same idea as pandas operations on columns.

All the examples use this data:

const products = [
  { name: "Arabica Gayo", category: "coffee", price: 95000, stock: 12 },
  { name: "Toraja Kalosi", category: "coffee", price: 110000, stock: 0 },
  { name: "French Press", category: "equipment", price: 245000, stock: 4 },
  { name: "Paper filters", category: "equipment", price: 35000, stock: 40 },
];
console.log(products.length);
4

Each block below repeats the data so it runs on its own.

map: transform every item

The product page needs a list of just the product names, and the price list needs every price with VAT added. Both are "do the same thing to every item and keep the results". That's map:

const products = [
  { name: "Arabica Gayo", category: "coffee", price: 95000, stock: 12 },
  { name: "Toraja Kalosi", category: "coffee", price: 110000, stock: 0 },
  { name: "French Press", category: "equipment", price: 245000, stock: 4 },
  { name: "Paper filters", category: "equipment", price: 35000, stock: 40 },
];

const names = products.map((p) => p.name);
const withVat = products.map((p) => ({ ...p, price: Math.round(p.price * 1.11) }));
console.log(names);
console.log(withVat[0].price, products[0].price);
["Arabica Gayo", "Toraja Kalosi", "French Press", "Paper filters"]
105450 95000

map returns a new array of the same length, with each item replaced by what the callback returns. The original isn't changed. (To return an object from an arrow function, wrap it in brackets: ({ ... }), otherwise the braces are read as a function body.)

filter, find, some, every

The next family of questions picks items out rather than changing them. Which products are in stock? What's the first product over Rp 100.000? Is anything sold out? Are all prices positive? Each question has its own method:

const products = [
  { name: "Arabica Gayo", category: "coffee", price: 95000, stock: 12 },
  { name: "Toraja Kalosi", category: "coffee", price: 110000, stock: 0 },
  { name: "French Press", category: "equipment", price: 245000, stock: 4 },
  { name: "Paper filters", category: "equipment", price: 35000, stock: 40 },
];

console.log(products.filter((p) => p.stock > 0).length);
console.log(products.find((p) => p.price > 100000).name);
console.log(products.find((p) => p.price > 1000000));
console.log(products.some((p) => p.stock === 0), products.every((p) => p.price > 0));
3
Toraja Kalosi
undefined
true true

reduce: combine into one value

Some questions boil a whole list down to one answer: the total value of all the stock, or how many products there are in each category. reduce walks through the array and builds up that answer one item at a time:

const products = [
  { name: "Arabica Gayo", category: "coffee", price: 95000, stock: 12 },
  { name: "Toraja Kalosi", category: "coffee", price: 110000, stock: 0 },
  { name: "French Press", category: "equipment", price: 245000, stock: 4 },
  { name: "Paper filters", category: "equipment", price: 35000, stock: 40 },
];

const stockValue = products.reduce((sum, p) => sum + p.price * p.stock, 0);
console.log(stockValue);

const countByCategory = products.reduce((counts, p) => {
  counts[p.category] = (counts[p.category] ?? 0) + 1;
  return counts;
}, {});
console.log(countByCategory);
3520000
{ coffee: 2, equipment: 2 }

reduce(callback, start) walks through the array carrying an accumulator: the callback receives the accumulator so far and the current item, and returns the new accumulator. Always pass the starting value (0, {}, []). reduce is powerful but the least readable of the methods; for a simple sum it's fine, for anything complex a for...of loop is often clearer.

sort (carefully)

Sorting looks simple, and that's what makes it dangerous in JavaScript. The first line below sorts four numbers without any extra instructions. Look closely at the result before reading on:

console.log([10, 9, 100, 25].sort());
console.log([10, 9, 100, 25].sort((a, b) => a - b));

const products = [
  { name: "Arabica Gayo", price: 95000 },
  { name: "French Press", price: 245000 },
  { name: "Paper filters", price: 35000 },
];
const cheapestFirst = products.toSorted((a, b) => a.price - b.price);
console.log(cheapestFirst.map((p) => p.name));
console.log(products[0].name);
console.log(["Toraja", "arabica", "Gayo"].toSorted((a, b) => a.localeCompare(b)));
[10, 100, 25, 9]
[9, 10, 25, 100]
["Paper filters", "Arabica Gayo", "French Press"]
Arabica Gayo
["arabica", "Gayo", "Toraja"]

Two traps:

  1. Without a compare function, sort compares items as strings, so 100 comes before 25. For numbers, always pass (a, b) => a - b (ascending) or b - a (descending).
  2. sort changes the original array. toSorted (newer) returns a sorted copy and leaves the original alone. Use it, or copy first: [...arr].sort(...).

For text, a.localeCompare(b) sorts alphabetically, ignoring case the way people expect.

Chaining into pipelines

Real pages combine these steps. The shop's "available products" list should only show items in stock, most expensive first, each as a nicely formatted label. Because every step returns a new array, you can chain them one after another:

const products = [
  { name: "Arabica Gayo", category: "coffee", price: 95000, stock: 12 },
  { name: "Toraja Kalosi", category: "coffee", price: 110000, stock: 0 },
  { name: "French Press", category: "equipment", price: 245000, stock: 4 },
  { name: "Paper filters", category: "equipment", price: 35000, stock: 40 },
];

const labels = products
  .filter((p) => p.stock > 0)
  .toSorted((a, b) => b.price - a.price)
  .map((p) => `${p.name} (Rp ${p.price.toLocaleString("id-ID")})`);
console.log(labels);
products.forEach((p, i) => console.log(i, p.name));
["French Press (Rp 245.000)", "Arabica Gayo (Rp 95.000)", "Paper filters (Rp 35.000)"]
0 Arabica Gayo
1 Toraja Kalosi
2 French Press
3 Paper filters

Each step returns a new array, so they chain. One step per line keeps the pipeline readable.

forEach runs a function for each item and returns nothing. It's for side effects (logging, updating the page). If you're building a new array, use map or filter instead of forEach + push.

Key idea

map transforms, filter keeps, find gets one, some/every test, reduce combines, toSorted orders (with a compare function!). They return new arrays and chain into pipelines; forEach is only for side effects.

Why this matters for clean code

Array methods make code declarative: they state what you want rather than how to loop. They also avoid mutation by default, which removes a whole family of bugs where a function quietly changes data its caller still uses. Give your callbacks names when they carry business rules (const isAvailable = (p) => p.stock > 0), and pipelines become readable specifications.

Recap

Practise this module

The questions for this lesson run in your browser and check your answer straight away.