Skip to content
JavaScript js functional 4 min read

Thinking in map / filter / reduce

Most data work is some combination of three operations: transform each item, keep some items, and collapse everything into one value. Imperative for loops mix these concerns together with index bookkeeping and a mutable accumulator. The array methods map, filter, and reduce give each operation its own name, so a chain of them reads like a description of what you want rather than how to build it. Learning to see a loop as a pipeline of these three verbs is one of the highest-leverage habits in functional JavaScript.

The three verbs

Each method takes a callback and returns a new value without mutating the original array. That immutability is what makes them safe to chain.

MethodInput → outputCallback returnsUse it to
maparray → array (same size)the transformed itemreshape every element
filterarray → array (≤ size)a boolean (keep?)select a subset
reducearray → any single valuethe next accumulatorfold many values into one

From an imperative loop to a pipeline

Consider a classic task: from a list of orders, total the value of completed orders, applying tax. The imperative version interleaves the filtering, the math, and the accumulation.

const orders = [
  { id: 1, status: "complete", amount: 50 },
  { id: 2, status: "pending",  amount: 80 },
  { id: 3, status: "complete", amount: 30 },
];

// Imperative: one loop doing three jobs at once.
let total = 0;
for (let i = 0; i < orders.length; i++) {
  if (orders[i].status === "complete") {
    total += orders[i].amount * 1.1;
  }
}
console.log(total.toFixed(2));

The same logic as a pipeline separates the select, transform, and fold steps into distinct, individually readable stages.

const orders = [
  { id: 1, status: "complete", amount: 50 },
  { id: 2, status: "pending",  amount: 80 },
  { id: 3, status: "complete", amount: 30 },
];

const total = orders
  .filter((o) => o.status === "complete") // select
  .map((o) => o.amount * 1.1)             // transform
  .reduce((sum, amount) => sum + amount, 0); // fold

console.log(total.toFixed(2));

Output:

88.00

Each line now answers a single question. You can comment out a stage, test it in isolation, or reorder the filter/map without untangling loop internals.

Understanding reduce

reduce is the most general of the three — map and filter can both be expressed in terms of it. Its callback receives the accumulator and the current item, and whatever it returns becomes the accumulator for the next step. Always pass an explicit initial value as the second argument; omitting it on an empty array throws a TypeError.

const words = ["build", "ship", "iterate"];

// Group/aggregate into a shape the input didn't have.
const byLength = words.reduce((acc, word) => {
  const len = word.length;
  (acc[len] ??= []).push(word);
  return acc;
}, {});

console.log(byLength);

Output:

{ '4': [ 'ship' ], '5': [ 'build' ], '7': [ 'iterate' ] }

The accumulator does not have to match the element type. Here an array of strings folds into an object of buckets — something map and filter alone cannot produce, since they always return arrays.

Reach for reduce only when the result type differs from the input (a number, an object, a Map). If you are returning an array of the same length, map is clearer; if a shorter array, filter. Overusing reduce to do everything produces dense, hard-to-read code.

Chaining and readability

Chaining works because each method returns a fresh array. Order matters for both clarity and cost: filter early to shrink the data before the more expensive map runs.

const users = [
  { name: "Ada",  active: true,  posts: 12 },
  { name: "Lin",  active: false, posts: 3 },
  { name: "Omar", active: true,  posts: 7 },
];

const topActiveNames = users
  .filter((u) => u.active)        // drop inactive first
  .filter((u) => u.posts >= 5)    // then the cheaper predicate
  .map((u) => u.name.toUpperCase());

console.log(topActiveNames);

Output:

[ 'ADA', 'OMAR' ]

Performance trade-offs

A chain of n methods makes n passes over the data and allocates an intermediate array at each step. A single for loop makes one pass with no allocations. For typical UI-sized arrays (hundreds to low thousands of items) this difference is irrelevant and readability wins. For very large arrays in hot paths, the cost can matter.

The middle ground is to fuse the steps into one reduce so you get a single pass while keeping declarative intent:

const nums = [1, 2, 3, 4, 5, 6];

// filter even + map square + sum, in one pass.
const result = nums.reduce((sum, n) => {
  if (n % 2 === 0) sum += n * n;
  return sum;
}, 0);

console.log(result); // 4 + 16 + 36

Output:

56

For lazy, on-demand processing of huge or infinite sequences, generators or a transducer library let you avoid intermediate arrays entirely — but only adopt that complexity once profiling proves the chain is a bottleneck.

Best practices

  • Read a loop as three questions — transform? select? collapse? — and map each to map, filter, or reduce.
  • Keep callbacks pure: no mutating outer state, no side effects inside the pipeline.
  • Always supply reduce’s initial value, and use reduce only when the output type differs from the input.
  • Filter before you map so expensive transforms run on the smallest possible set.
  • Prefer a chain for clarity; collapse to a single reduce or for loop only when profiling shows it matters.
  • Name intermediate results (or extract named callbacks) when a chain grows past three or four stages.
Last updated June 1, 2026
Was this helpful?