Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

JavaScript (ES2026)  Module 6 – Arrays and array methods

reduce in depth

Use JavaScript reduce to sum, group, index and flatten arrays, always with an initial value, avoid the quadratic spread trap, and know when a loop reads better.

  • Intermediate
  • 25 minutes
  • Examples run with Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free lesson, Pro answers: 25 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Use reduce to sum, group, index and flatten data
  • Pass an initial value every time, and explain what goes wrong without one
  • Recognise when reduce hurts readability, and choose a clearer method

Before you start

reduce() turns a whole array into one value by carrying an accumulator from item to item. You give it a function and a starting value: spends.reduce((sum, amount) => sum + amount, 0) calls the function once for each item with the accumulator so far and the next item, whatever the function returns becomes the accumulator of the next call, and the value of the last call is the result. The accumulator can be a number for a sum, an object or a Map for totals per category or an index by id, or an array for a flattened list. Always pass the starting value: without it, reduce() starts from the first item, gives wrong answers on arrays of objects and throws a TypeError on an empty array. This lesson builds each kind of accumulator, measures the spread pattern that makes reduce() quadratic, and shows when Object.groupBy() or a plain loop reads better.

How reduce() runs

A sum, one call at a time JavaScript · reduce-steps.mjs
// reduce() carries one value, the accumulator, from item to item. Each call gets the accumulator and the next item
// and returns the new accumulator; what the last call returns is the result.
const spends = [450, 1200, 300];

const total = spends.reduce((sum, amount, index) => {
  const next = sum + amount;
  console.log(`call ${index + 1}: sum ${String(sum).padStart(4)} + amount ${String(amount).padStart(4)} = ${next}`);
  return next;
}, 0);

console.log('total:', total);

Output

call 1: sum    0 + amount  450 = 450
call 2: sum  450 + amount 1200 = 1650
call 3: sum 1650 + amount  300 = 1950
total: 1950

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node reduce-steps.mjs

The second argument, 0, is the initial value: it is the accumulator of the first call. That call adds the first amount and returns 450, which becomes sum in the second call, and so on. When no items are left, reduce() returns whatever the last call returned, 1950. The function receives four arguments: the accumulator, the item, its index (the trace prints it) and the array. Like map() and filter(), reduce() does not change the array.

Diagram: reduce starts from 0, adds 450, 1200 and 300 in three calls, passing each sum on, and returns 1950.initial value: 0call 1: 0 + 450returns 450call 2: 450 + 1200returns 1650call 3: 1650 + 300returns 1950reduce() returns 1950accumulatoraccumulatoraccumulatorno items left

The accumulator passed from call to call

Text description of the diagram

The diagram is a column of boxes joined by arrows labelled "accumulator".

  • It starts with the initial value, 0.
  • Call 1 adds the first spend, 450, to 0 and returns 450.
  • Call 2 adds 1200 to 450 and returns 1650.
  • Call 3 adds 300 to 1650 and returns 1950.

With no items left, reduce() returns the value of the last call, 1950. Every call has to return the accumulator: whatever it returns is what the next call receives.

The one rule inside the function: return the accumulator, every time. Whatever the function returns is all the next call gets. An arrow function with a block body, (acc, n) => { acc.push(n); }, returns undefined, and the next call then fails on undefined.push.

Always pass an initial value

The initial value is optional, and leaving it out causes a whole family of reduce() bugs. Without it, the first item becomes the starting accumulator and the function is first called with the second item:

Three ways to go wrong without an initial value JavaScript · initial-value.mjs
// Without an initial value, reduce() starts with the first item as the accumulator. That goes wrong in three ways.
const expenses = [
  { category: 'food', amountPaise: 45000 },
  { category: 'travel', amountPaise: 120000 },
];

// 1. The first item is an object, so the first "sum" is an object plus a number.
console.log('no initial value:', expenses.reduce((sum, e) => sum + e.amountPaise));
console.log('initial value 0: ', expenses.reduce((sum, e) => sum + e.amountPaise, 0));

// 2. With one item, reduce() returns that item without calling the function at all.
console.log('one item:', [{ category: 'rent', amountPaise: 900000 }].reduce((sum, e) => sum + e.amountPaise));

// 3. With no items, there is nothing to start from: a TypeError.
try {
  [].reduce((sum, e) => sum + e.amountPaise);
} catch (error) {
  console.log('no items:', error.name, '-', error.message);
}
console.log('no items, initial value 0:', [].reduce((sum, e) => sum + e.amountPaise, 0));

Output

no initial value: [object Object]120000
initial value 0:  165000
one item: { category: 'rent', amountPaise: 900000 }
no items: TypeError - Reduce of empty array with no initial value
no items, initial value 0: 0

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node initial-value.mjs

Version note

In the Run button’s engine, QuickJS 0.32.0, the message is empty array. The language fixes only the type of the error, a TypeError; each engine writes its own message.

In your browser, QuickJS (quickjs-emscripten 0.32.0) prints:

no initial value: [object Object]120000
initial value 0:  165000
one item: { category: 'rent', amountPaise: 900000 }
no items: TypeError - empty array
no items, initial value 0: 0
  1. Wrong types. The first accumulator is the first expense, an object, so the first “sum” is an object plus a number. JavaScript turns the object into the text [object Object] and joins the number to it: no error, just a string where a total should be. With 0 as the initial value, the sum is 165000.
  2. One item. With a single item and no initial value, reduce() returns that item without calling the function at all, so a list with one expense “totals” to the expense object itself.
  3. No items. With neither items nor an initial value there is nothing to start from, and the specification requires a TypeError. A month with no expenses crashes the report; with an initial value it totals to 0.

Pass the initial value every time, of the same type as the result: 0 for a sum, new Map() or {} for totals and indexes, [] for a list. Then every one of the three cases above gives the answer you meant.

Totals per category: an object accumulator, or Object.groupBy

A trip's spending per category, two ways JavaScript · expense-totals.mjs
// A trip's expenses in paise: the total per category with reduce(), and the same with Object.groupBy().
const expenses = [
  { category: 'food', amountPaise: 45000 },
  { category: 'travel', amountPaise: 120000 },
  { category: 'food', amountPaise: 30000 },
  { category: 'stay', amountPaise: 250000 },
  { category: 'travel', amountPaise: 8000 },
];

const totals = expenses.reduce((acc, e) => {
  acc[e.category] = (acc[e.category] ?? 0) + e.amountPaise;
  return acc;
}, {});
console.log('reduce:', totals);

const groups = Object.groupBy(expenses, (e) => e.category);
console.log('groupBy:', Object.keys(groups), '| food has', groups.food.length, 'items');

const fromGroups = {};
for (const [category, items] of Object.entries(groups)) {
  fromGroups[category] = items.reduce((sum, e) => sum + e.amountPaise, 0);
}
console.log('groupBy, then one sum per group:', fromGroups);
console.log('food in rupees:', (totals.food / 100).toFixed(2));

Output

reduce: { food: 75000, travel: 128000, stay: 250000 }
groupBy: [ 'food', 'travel', 'stay' ] | food has 2 items
groupBy, then one sum per group: { food: 75000, travel: 128000, stay: 250000 }
food in rupees: 750.00

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node expense-totals.mjs

The reduce() version starts from an empty object and, for each expense, adds its amount to the total of its category: acc[e.category] ?? 0 gives 0 the first time a category is seen. The function changes the accumulator and returns it. That is safe here: the object was created for this one reduce() call, so no other code can see it change. A {} accumulator has a different weakness, its inherited keys, which the next section shows.

Object.groupBy(), added in ECMAScript 2024, does the grouping part for you: it returns an object whose keys are the categories and whose values are arrays of the matching expenses. Totals then take one small reduce() per group. Which is better depends on what you need. When you want the groups themselves, to list the food expenses or count them, Object.groupBy() says so in one call. When you want only the totals, a single reduce() gets there in one pass without keeping every group in memory. The object that Object.groupBy() returns has no prototype, so a category named toString cannot clash with anything; the grouping lesson of this module covers it, and Map.groupBy(), in depth.

Version note

Object.groupBy() and Map.groupBy() need Node.js 21 or later, Chrome 117, Firefox 119 or Safari 17.4.

An index by id, and counting with a Map

A Map is often the best accumulator: it accepts any key, and its set() method returns the Map itself, so one expression can update the accumulator and return it:

A lookup Map from an array, and a count of tags JavaScript · index-by-id.mjs
// Index users by id once, then look each one up without searching the array.
const users = [
  { id: 'u17', name: 'Asha' },
  { id: 'u42', name: 'Ravi' },
  { id: 'u08', name: 'Meena' },
];

const byId = users.reduce((map, user) => map.set(user.id, user), new Map());
console.log(byId.get('u42').name, byId.size, byId.has('u99'));

const sameWithoutReduce = new Map(users.map((user) => [user.id, user]));
console.log(sameWithoutReduce.get('u08').name);

// Counting with a Map accumulator: any key is safe, 'toString' included.
const tags = ['news', 'toString', 'news', 'sport'];
const counts = tags.reduce((acc, tag) => acc.set(tag, (acc.get(tag) ?? 0) + 1), new Map());
console.log(counts);

// The same count on a {} accumulator finds an inherited method under 'toString'.
const unsafe = tags.reduce((acc, tag) => {
  acc[tag] = (acc[tag] ?? 0) + 1;
  return acc;
}, {});
console.log("with {}, the count for 'toString' is a", typeof unsafe.toString);

Output

Ravi 3 false
Meena
Map(3) { 'news' => 2, 'toString' => 1, 'sport' => 1 }
with {}, the count for 'toString' is a string

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node index-by-id.mjs

users.reduce((map, user) => map.set(user.id, user), new Map()) builds the index in one pass, and from then on byId.get('u42') finds a user without searching the array. The same Map can be built without reduce(), by turning each user into a [key, value] pair and passing the pairs to new Map(), which says more directly what it builds.

The tag count shows why a plain object is a risky accumulator for keys that come from data. {} inherits properties from Object.prototype, among them toString and constructor. For the tag 'toString', acc[tag] ?? 0 finds the inherited method instead of undefined, adds 1 to a function and stores a string. A Map has no such keys, and neither does an object made with Object.create(null) or returned by Object.groupBy().

Other shapes: flatten, maximum, running totals, pipelines

Arrays, numbers and functions as accumulators JavaScript · more-shapes.mjs
// The accumulator can be any value: an array to flatten into, a running maximum, a list of balances, or a value
// passed through a list of functions.
const baskets = [['rice', 'dal'], ['tea'], ['salt', 'oil']];
console.log('flatten:', baskets.reduce((all, basket) => all.concat(basket), []));
console.log('flat():  ', baskets.flat());

const readings = [31, 29, 35, 33];
console.log('largest:', readings.reduce((max, n) => (n > max ? n : max), -Infinity));

const changes = [500, -200, -100, 1000];
const balances = changes.reduce((acc, change) => {
  acc.push((acc.at(-1) ?? 0) + change);
  return acc;
}, []);
console.log('balance after each change:', balances);

const steps = [(x) => x + 1, (x) => x * 2];
console.log('reduce:', steps.reduce((value, step) => step(value), 5), '| reduceRight:', steps.reduceRight((value, step) => step(value), 5));

Output

flatten: [ 'rice', 'dal', 'tea', 'salt', 'oil' ]
flat():   [ 'rice', 'dal', 'tea', 'salt', 'oil' ]
largest: 35
balance after each change: [ 500, 300, 200, 1200 ]
reduce: 12 | reduceRight: 11

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node more-shapes.mjs

  • Flatten: concat() joins each basket onto the list so far. flat() does the same in one call and reads better, so use it; the module’s lesson on flat() and Array.from() covers it.
  • Maximum: a running maximum with -Infinity as the initial value, which every number beats. Unlike Math.max(...readings), it works on arrays of any size, because spreading a very large array into one call can hit the engine’s limit on arguments.
  • Running totals: the accumulator is an array of balances, and each call adds the previous balance, acc.at(-1) ?? 0, to the next change.
  • Pipelines: the accumulator is a value passed through a list of functions. reduceRight() works the same way from the last item to the first, so the two directions give different results: (5 + 1) × 2 is 12, while 5 × 2 + 1 is 11.

The spread trap: a reduce() that slows down quadratically

One way to build an object with reduce() copies the accumulator on every call, so that nothing is ever changed in place: rows.reduce((acc, row) => ({ ...acc, [row.id]: row }), {}). It looks tidy and gives the right answer, and on a large array it is very slow:

An index of up to 2,000 rows, with and without the spread JavaScript · spread-accumulator.mjs
// Indexing rows by id with reduce(): copying the accumulator with spread on every row, against adding to one object.
// Times are in milliseconds; the one-object version is timed three times and the fastest run is shown.
const makeRows = (n) => Array.from({ length: n }, (_, i) => ({ id: `u${i}`, name: `User ${i}` }));

const withSpread = (rows) => rows.reduce((acc, row) => ({ ...acc, [row.id]: row }), {});
const withOneObject = (rows) =>
  rows.reduce((acc, row) => {
    acc[row.id] = row;
    return acc;
  }, {});

function time(task, rows, runs) {
  let best = Infinity;
  for (let run = 0; run < runs; run++) {
    const start = performance.now();
    const index = task(rows);
    best = Math.min(best, performance.now() - start);
    if (Object.keys(index).length !== rows.length) throw new Error('a row is missing');
  }
  return best;
}

const fmt = (n) => n.toLocaleString('en-US');
console.log('  rows    spread copy   one object   properties the spread copied');
for (const n of [250, 500, 1000, 2000]) {
  const rows = makeRows(n);
  const spread = time(withSpread, rows, 1).toFixed(1).padStart(9);
  const one = time(withOneObject, rows, 3).toFixed(2).padStart(8);
  console.log(`${fmt(n).padStart(6)}  ${spread} ms  ${one} ms   ${fmt((n * (n - 1)) / 2).padStart(12)}`);
}

Output

  rows    spread copy   one object   properties the spread copied
   250        3.1 ms      0.04 ms         31,125
   500       13.0 ms      0.04 ms        124,750
 1,000      155.8 ms      0.07 ms        499,500
 2,000     1195.1 ms      0.14 ms      1,999,000

This output changes from run to run: the times depend on the computer and on what else it is running

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node spread-accumulator.mjs

The spread on row k copies the k − 1 properties already in the accumulator into a new object, so the copies add up to n × (n − 1) ÷ 2: 499,500 for 1,000 rows and 1,999,000 for 2,000. The work grows with the square of the number of rows, and the measured time grew faster still: in the run above, each doubling of the rows made the spread version several times slower, and 2,000 rows took over a second, while adding each row to one object stayed under a millisecond at every size.

The fix is the one the totals example already uses: change the accumulator you created as the initial value, and return it. Nothing outside the reduce() call can see that object until it is finished, so changing it in place has none of the risks of changing data that other code holds. For an index, new Map(rows.map((row) => [row.id, row])) and Object.fromEntries(rows.map((row) => [row.id, row])) are one pass each and say what they build.

When reduce() hurts readability

reduce() can do the work of almost every other array method, and that is the trouble: a reader has to work out what the accumulator is before they know what the code does. Prefer the method that names the job:

The job With reduce() Clearer
Add up numbers reduce((s, n) => s + n, 0) reduce() is fine here
Group items reduce() into an object of arrays Object.groupBy(), Map.groupBy()
Index by a key reduce() into a Map new Map(list.map(…))
Flatten one level reduce() with concat() flat()
Yes-or-no questions reduce() into a boolean some(), every()
Several results at once an accumulator object with many fields a for…of loop with named variables

A reduce() whose function runs past a few lines, keeps several values in one accumulator, or contains another reduce(), is usually clearer as a for…of loop with a well-named variable or two. The loop does the same work in one pass, and a reader can follow it line by line.

Itemised Bill Split Calculator Split a shared trip's expenses between friends, then check the totals per person with a reduce() of your own. Count Unique Values (Frequency Counter) Count how often each value appears in a column: the result a reduce() with a Map accumulator builds. JavaScript & TypeScript Online Compiler Run spread-accumulator.mjs with 4,000 rows and compare the two versions on your own computer.

Exercises

Exercise · Easy · JavaScript

Total a month's expenses per category in a Map

Write totalByCategory(expenses) in totals.mjs. Each expense is an object such as { category: 'food', amountPaise: 45000 }, and the function returns a Map from each category to the total of its amounts in paise, with the categories in the order they first appear.

  • totalByCategory([{ category: 'food', amountPaise: 45000 }, { category: 'food', amountPaise: 30000 }]) returns new Map([['food', 75000]]).
  • An expense whose category is missing or an empty string counts as 'other'.
  • Any category name works, 'constructor' and 'toString' included.
  • amountPaise must be a whole number: anything else, such as 99.5 or '500', throws a TypeError.
  • An empty list gives an empty Map, and the expenses passed in must not change.

The starter works for clean data but has no 'other' category and accepts any amount. Run the sample tests when you are done.

Starter code · totals.mjs

// The expense totals of lesson javascript/reduce-in-depth.

export function totalByCategory(expenses) {
  // Fine for clean data: there is no 'other' category, and any amount is added as it is.
  return expenses.reduce((totals, expense) => {
    totals.set(expense.category, (totals.get(expense.category) ?? 0) + expense.amountPaise);
    return totals;
  }, new Map());
}
The sample tests · totals.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { totalByCategory } from './totals.mjs';

const month = [
  { category: 'food', amountPaise: 45000 },
  { category: 'travel', amountPaise: 120000 },
  { category: 'food', amountPaise: 30000 },
  { category: 'stay', amountPaise: 250000 },
];

test('totals per category, in the order the categories first appear', () => {
  const totals = totalByCategory(month);
  assert.deepEqual(totals, new Map([['food', 75000], ['travel', 120000], ['stay', 250000]]));
  assert.deepEqual([...totals.keys()], ['food', 'travel', 'stay']);
});

test('names such as constructor and toString are ordinary categories', () => {
  const odd = [{ category: 'constructor', amountPaise: 500 }, { category: 'toString', amountPaise: 700 }, { category: 'constructor', amountPaise: 100 }];
  assert.deepEqual(totalByCategory(odd), new Map([['constructor', 600], ['toString', 700]]));
});

test('a missing or empty category counts as other', () => {
  const loose = [{ amountPaise: 1000 }, { category: '', amountPaise: 250 }, { category: 'food', amountPaise: 400 }];
  assert.deepEqual(totalByCategory(loose), new Map([['other', 1250], ['food', 400]]));
});

test('an amount that is not a whole number of paise is a TypeError', () => {
  assert.throws(() => totalByCategory([{ category: 'food', amountPaise: 99.5 }]), TypeError);
  assert.throws(() => totalByCategory([{ category: 'food', amountPaise: '500' }]), TypeError);
});

test('no expenses give an empty Map', () => {
  assert.deepEqual(totalByCategory([]), new Map());
});

test('the expenses passed in do not change', () => {
  const copy = month.map((e) => ({ ...e }));
  totalByCategory(month);
  assert.deepEqual(month, copy);
});
A hint

Start reduce() with new Map() as its initial value. In the function, work out the key first (the category, or 'other'), check the amount with Number.isInteger(), and finish with return totals.set(key, …): set() returns the Map itself, so that line both updates the totals and hands them to the next call.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

7 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 7 What does [1, 2, 3].reduce((acc, n) => acc + n, 10) return?

    Type a number.

    Show the answer to question 1

    Answer: 16

    The initial value 10 is the first accumulator. The calls return 11, 13 and 16, and the value of the last call is the result.

  2. Question 2 of 7 What happens when this line runs?

    Read the code, then choose one answer.

    const total = [].reduce((sum, n) => sum + n);
    Show the answer to question 2

    Answer: It throws a TypeError

    With no initial value, reduce() takes the first item as the starting accumulator, and an empty array has none, so the language requires a TypeError. Pass an initial value, reduce(fn, 0), and the result is 0.

  3. Question 3 of 7 What does this program print?

    Read the code, then choose one answer.

    const bills = [{ amount: 5 }, { amount: 7 }];
    
    console.log(bills.reduce((sum, b) => sum + b.amount));
    Show the answer to question 3

    Answer: [object Object]7

    Without an initial value, the first accumulator is the first object, { amount: 5 }. Adding a number to an object turns the object into the text "[object Object]" and joins the 7 to it. With reduce(fn, 0) the result is 12.

  4. Question 4 of 7 What happens when this program runs?

    Read the code, then choose one answer.

    const doubled = [1, 2, 3].reduce((acc, n) => {
      acc.push(n * 2);
    }, []);
    
    console.log(doubled);
    Show the answer to question 4

    Answer: It throws a TypeError on the second call

    The function has a block body and no return, so the first call returns undefined, and that becomes the accumulator of the second call. undefined.push(…) throws a TypeError. Every call must return the accumulator.

  5. Question 5 of 7 Which expressions build a Map from each user's id to the user? Choose every right one.

    Choose every answer that is right.

    Show the answer to question 5

    Answer:

    • users.reduce((m, u) => m.set(u.id, u), new Map())
    • new Map(users.map((u) => [u.id, u]))

    A Map can be made from [key, value] pairs, and set() returns the Map, so the short arrow function returns the accumulator. The version with braces returns nothing, so the second call gets undefined. Object.groupBy() returns an object whose values are arrays of users, not a Map.

  6. Question 6 of 7 What does reduce-steps.mjs print?

    What does this program print? Choose one answer.

    // reduce() carries one value, the accumulator, from item to item. Each call gets the accumulator and the next item
    // and returns the new accumulator; what the last call returns is the result.
    const spends = [450, 1200, 300];
    
    const total = spends.reduce((sum, amount, index) => {
      const next = sum + amount;
      console.log(`call ${index + 1}: sum ${String(sum).padStart(4)} + amount ${String(amount).padStart(4)} = ${next}`);
      return next;
    }, 0);
    
    console.log('total:', total);
    Show the answer to question 6

    Answer: it prints

    call 1: sum    0 + amount  450 = 450
    call 2: sum  450 + amount 1200 = 1650
    call 3: sum 1650 + amount  300 = 1950
    total: 1950

    With the initial value 0, the function is called once for every item, three times, and each call receives what the previous call returned. Without the initial value it would start from 450 and be called only twice.

  7. Question 7 of 7 rows.reduce((acc, r) => ({ ...acc, [r.id]: r }), {}) runs on 1,000 rows. How many properties do the spreads copy in total?

    Type a number.

    Show the answer to question 7

    Answer: 499500

    The spread on row k copies the k − 1 properties already in the accumulator, so the total is 0 + 1 + … + 999 = 1,000 × 999 ÷ 2 = 499,500. Doubling the rows to 2,000 makes it 1,999,000, about four times as many, and the measured time grew even faster than that.

Interview questions

Fresher · What does reduce() do in JavaScript, and why should you pass an initial value?

reduce(fn, initial) turns an array into one value. It calls fn once for each item with an accumulator and the item, and whatever fn returns becomes the accumulator for the next call; the last return value is the result. With initial given, the first call gets it as the accumulator. Without it, the first item is the starting accumulator, which goes wrong three ways: on an array of objects the first “sum” is an object plus a number, which gives a string such as "[object Object]7"; an array with one item returns that item without calling fn; and an empty array throws a TypeError. So pass an initial value of the result’s type every time: 0 for sums, new Map() or {} for totals, [] for lists, and return the accumulator from every call.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A helper builds a lookup with rows.reduce((acc, r) => ({ ...acc, [r.id]: r }), {}). It takes about a tenth of a second for 1,000 rows, but a customer with 20,000 rows reports the page freezing for minutes. Explain the growth with numbers, fix it, and say when the spread version is acceptable.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    const total = expenses.reduce((sum, e) => sum + e.amountPaise); shows "[object Object]120000" on the monthly report, and the same line crashes with a TypeError for a month with no expenses. Explain both failures, the fix, and one more case in which the missing initial value hides a bug.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • reduce(fn, initial) calls fn(accumulator, item, index, array) for each item; what fn returns is the next accumulator, and the last one is the result.
  • Pass an initial value every time. Without one, objects turn into [object Object], a single item is returned as it is, and an empty array throws a TypeError.
  • Return the accumulator from every call: a block body without return gives the next call undefined.
  • Change the accumulator you created and return it. Copying it with spread on every call made 2,000 rows take over a second in the run above; one object took under a millisecond.
  • Use a Map, Object.create(null) or Object.groupBy() when keys come from data: a {} accumulator inherits keys such as toString.
  • Prefer the method that names the job: Object.groupBy() to group, new Map(list.map(…)) to index, flat() to flatten, some() and every() for yes-or-no; switch to a loop when the accumulator gets complicated.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.