Skip to content
React Course1.0.0 beta
CSEN
React Course1.0.0 beta
CSEN
🏠 Home🧭 Where to start?🔁 Review📄 Cheat sheets📰 What's newℹ️ About the course
0. JavaScript0/22

JavaScript in general

  • 0.1 Variables, types and operators0/2
  • 0.2 Functions and closures0/2
  • 0.3 Objects0/2
  • 0.4 Arrays I: map, filter and friends0/2
  • 0.5 Arrays II: reduce, sorting and grouping0/3
  • 0.6 Conditions and logical operators0/2
  • 0.7 Asynchronous JavaScript0/2
  • 0.8 Modules and built-in tools0/2

JavaScript for React

  • 0.9 References and immutability0/3
  • 0.10 JS patterns you meet everywhere in React0/2

Basics

  • 1. A quick review of the basics0/3
  • 2. State and events (useState)0/3

Hooks in depth

  • 3. Effects (useEffect)0/3
  • 4. Refs (useRef)0/3
  • 5. More complex state (useReducer)0/3
  • 6. Context0/3
  • 7. Custom hooks0/3

Forms

  • 8. Forms and React 19 Actions0/3

Performance

  • 9. Performance and rendering0/3

Patterns

  • 10. Component design patterns0/3

Ecosystem

  • 11. Where the type goes (TypeScript syntax)0/3
  • 12. TypeScript with React0/3
  • 13. Routing (React Router)0/3
  • 14. Data fetching0/3
  • 15. Application state management0/3

Project

  • 16. Final project: Kanban0/2

Summary

  • 17. Summary: the principles of React

Your account

Privacy

Lesson 0.5 · JavaScript in general

Arrays II: reduce, sorting and grouping

reduce in depth, toSorted, Object.groupBy, Set, flatMap and the methods that change an array.

Loading lesson…

💬 Found a mistake, something unclear, or have an idea? Let me know.
← Arrays I: map, filter and friendsConditions and logical operators →

Before we start: from many to one

In the last lesson an array always turned into another array (map, filter), one item (find) or yes/no (some). But often you need to make one summary value from the whole array: the total price of the cart, the most expensive item, counts per category.

Picture a cashier ringing up your shopping. They start with zero on the display and add the price of every item on the belt. At the end they show one number. That's exactly what reduce does – and because the "running total" can be anything (a number, an object, an array), it can do surprisingly much.

The second half of the lesson is about sorting, grouping and the methods that quietly change the original array – and their safe replacements.

ℹ️ What's in this lesson
  1. reduce step by step.
  2. Everything you can calculate with reduce.
  3. Grouping: Object.groupBy.
  4. Sorting: the compare function, numbers, text, several criteria.
  5. Slicing and joining: slice, spread, flat, flatMap.
  6. Unique values with Set.
  7. Methods that change the array, and their replacements.

1. reduce step by step

const prices = [12, 15, 6]
const total = prices.reduce((sum, price) => sum + price, 0)
// ↑ ↑ ↑
// accumulator item initial value
// total = 33

reduce gets two things: a function and the initial value of the accumulator. The function is called for every item. It gets "the result so far" (the accumulator) and the current item and returns the new result so far. That's what it gets on the next call.

Stepsum (accumulator)pricethe function returns
1.0 (initial value)120 + 12 = 12
2.121512 + 15 = 27
3.27627 + 6 = 33
end33 – the result of reduce
⚠️ Always give an initial value
[].reduce((sum, n) => sum + n) // 💥 TypeError: Reduce of empty array with no initial value
[].reduce((sum, n) => sum + n, 0) // ✅ 0
products.reduce((sum, p) => sum + p.price) // ❌ without 0 the first accumulator is the WHOLE first object
products.reduce((sum, p) => sum + p.price, 0) // ✅
⚠️ The function must return the accumulator
items.reduce((sum, item) => { sum + item.price }, 0) // ❌ return is missing → NaN
items.reduce((sum, item) => sum + item.price, 0) // ✅
items.reduce((sum, item) => { return sum + item.price }, 0) // ✅

2. Everything reduce can calculate

Numbers: sum, average, maximum

const cart = [
{ name: 'Apple', price: 12, qty: 3 },
{ name: 'Bread', price: 45, qty: 1 },
{ name: 'Milk', price: 22, qty: 2 },
]
const total = cart.reduce((sum, item) => sum + item.price * item.qty, 0) // 125
const count = cart.reduce((sum, item) => sum + item.qty, 0) // 6
const average = total / count // 20.83…
// Maximum: the accumulator remembers the biggest so far
const maxPrice = cart.reduce((max, item) => (item.price > max ? item.price : max), 0) // 45
// The most expensive ITEM (an object): the accumulator is the most expensive item so far
const priciest = cart.reduce((best, item) => (item.price > best.price ? item : best)) // Bread
// (without an initial value it starts with the first element – fine here, as long as the array isn't empty)
✨ For the maximum and minimum of numbers, Math is enough
const prices = cart.map((item) => item.price) // [12, 45, 22]
Math.max(...prices) // 45 – spread unpacks the array into separate arguments
Math.min(...prices) // 12

An object: counts and sums by key

const votes = ['yes', 'no', 'yes', 'abstain', 'yes']
const counts = votes.reduce((acc, vote) => {
acc[vote] = (acc[vote] ?? 0) + 1
return acc // don't forget to return the accumulator!
}, {})
// { yes: 3, no: 1, abstain: 1 }
// The same without changing the accumulator (spread) – longer, but "pure"
const counts = votes.reduce((acc, vote) => ({ ...acc, [vote]: (acc[vote] ?? 0) + 1 }), {})
❓ Can I change the accumulator inside reduce (acc[vote] = …)?

Yes – the accumulator is an object we created ourselves as the initial value {}; it belongs to nobody else. Changing it is faster than copying it every time. What you mustn't change are the array's items (vote, item) – those belong to someone else.

A dictionary by id

const users = [{ id: 7, name: 'Anna' }, { id: 9, name: 'Peter' }]
const byId = users.reduce((acc, user) => {
acc[user.id] = user
return acc
}, {})
// { 7: { id: 7, name: 'Anna' }, 9: { id: 9, name: 'Peter' } }
byId[9].name // 'Peter' – a fast lookup without going through the array
// A shorter alternative without reduce:
Object.fromEntries(users.map((u) => [u.id, u]))
Demo

reduce stepper

What you see: Four items and four different reducers. The table shows what was in the accumulator before every step and what the function returned.

Try it:

  1. With sum, keep clicking Next step. The accumulator grows 0 → 12 → 18 → 33 → 42.
  2. Switch to maximum: the accumulator only changes when a more expensive item comes along.
  3. Switch to counts: the accumulator is an object and every step raises one category in it.
  4. Grouping: the accumulator is an object with arrays of names. The built-in Object.groupBy can do this too.

The takeaway: reduce is a loop with a memory. The function always gets what it returned last time and returns a new version. The initial value decides the type of the accumulator (a number, an object, an array).

Loading the interactive part…

✨ reduce isn't always the most readable
If you get lost in a reduce, it's fine to write a plain for…of loop with a variable. The result is the same and a colleague will read it faster. For sums, counts and dictionaries, though, reduce is the standard – it's worth getting to know.

3. Grouping: Object.groupBy

Modern JavaScript can split an array into groups by some property with a single call:

const products = [
{ name: 'Apple', category: 'fruit' },
{ name: 'Carrot', category: 'vegetables' },
{ name: 'Pear', category: 'fruit' },
]
const groups = Object.groupBy(products, (p) => p.category)
// {
// fruit: [{ name: 'Apple', … }, { name: 'Pear', … }],
// vegetables: [{ name: 'Carrot', … }],
// }
// The group can be decided by a calculation too
Object.groupBy(products, (p) => (p.name.length > 5 ? 'long' : 'short'))
// Typical use: go through the groups
for (const [category, items] of Object.entries(groups)) {
console.log(category, items.map((p) => p.name).join(', '))
}
// fruit Apple, Pear
// vegetables Carrot

4. Sorting

You sort with the toSorted method (a new array) or the older sort (changes the original array). Both get a compare function: it gets two items a and b and returns a number.

The function returnsMeaning
a negative numbera goes BEFORE b
a positive numbera goes AFTER b
0it doesn't matter, keep the order
const prices = [40, 5, 100, 25]
prices.toSorted((a, b) => a - b) // [5, 25, 40, 100] – ascending
prices.toSorted((a, b) => b - a) // [100, 40, 25, 5] – descending
prices // [40, 5, 100, 25] – toSorted doesn't change the original
// Why a - b? When a < b, the result is negative → a goes first. Exactly what we want.
⚠️ Without a compare function it sorts as TEXT
[40, 5, 100, 25].toSorted() // [100, 25, 40, 5] 😱 – compares '100' < '25' < '40' < '5'
For numbers, always pass (a, b) => a - b.

Sorting text and objects

const names = ['Zoe', 'adam', 'Émile', 'Bella']
names.toSorted() // ['Bella', 'Zoe', 'adam', 'Émile'] 😕 by character codes
names.toSorted((a, b) => a.localeCompare(b, 'en')) // ['adam', 'Bella', 'Émile', 'Zoe'] ✅ alphabetical order
const users = [
{ name: 'Peter', age: 25 },
{ name: 'Anna', age: 31 },
{ name: 'Eve', age: 25 },
]
users.toSorted((a, b) => a.age - b.age) // by age
users.toSorted((a, b) => a.name.localeCompare(b.name, 'en')) // by name
// By two criteria: age first, the name when they match
users.toSorted((a, b) => a.age - b.age || a.name.localeCompare(b.name, 'en'))
// Eve (25), Peter (25), Anna (31)
// when a.age - b.age is 0 (falsy), the part after || is used
// By date
orders.toSorted((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) // newest first
// Booleans: unfinished first
tasks.toSorted((a, b) => Number(a.done) - Number(b.done))
❓ sort, or toSorted?

sort sorts the array in place – it changes it – and returns that same array. toSorted (newer) returns a sorted copy and leaves the original alone. In older code you'll see [...items].sort(…) – a copy with spread and then sort – which is the same as items.toSorted(…).

5. Slicing, joining and flattening

const letters = ['a', 'b', 'c', 'd', 'e']
letters.slice(1, 3) // ['b', 'c'] – from index 1 to 3 (3 not included), a new array
letters.slice(2) // ['c', 'd', 'e'] – from 2 to the end
letters.slice(-2) // ['d', 'e'] – the last two
letters.slice(0, 3) // ['a', 'b', 'c'] – the first three ("top 3")
letters.slice() // a copy of the whole array
const a = [1, 2]
const b = [3, 4]
[...a, ...b] // [1, 2, 3, 4] – joining
[0, ...a, 2.5, ...b] // [0, 1, 2, 2.5, 3, 4] – with values inserted
a.concat(b) // [1, 2, 3, 4] – the same, the old way
['Anna', 'Peter'].join(', ') // 'Anna, Peter'
['a', 'b'].join('') // 'ab'

flat and flatMap

[[1, 2], [3], [4, 5]].flat() // [1, 2, 3, 4, 5] – flattens by one level
const posts = [
{ title: 'A', tags: ['react', 'js'] },
{ title: 'B', tags: ['css'] },
]
posts.map((p) => p.tags) // [['react', 'js'], ['css']] – an array of arrays
posts.flatMap((p) => p.tags) // ['react', 'js', 'css'] – map + flat in one

6. Unique values: Set

Set is a collection that remembers every value only once. Ideal for removing duplicates:

const tags = ['react', 'js', 'react', 'css', 'js']
const unique = new Set(tags) // Set { 'react', 'js', 'css' }
unique.size // 3
unique.has('css') // true
[...unique] // ['react', 'js', 'css'] – back to an array
// The one-line classic
const uniqueTags = [...new Set(tags)]
// Unique categories from an array of objects
const categories = [...new Set(products.map((p) => p.category))]
⚠️ Set compares objects by reference
new Set([{ id: 1 }, { id: 1 }]) has a size of 2 – they're two different objects. For objects unique by id, use a dictionary (byId above) or a Set of just the ids.

7. Methods that change the array

A few older methods don't write a new array but overwrite the original one. In a plain script it doesn't matter, but when the array belongs to someone else (React state, say), it's a source of nasty bugs. Each one has a safe replacement:

❌ Changes the original array✅ Returns a new arrayWhat it does
arr.push(x)[...arr, x]adds to the end
arr.unshift(x)[x, ...arr]adds to the start
arr.pop()arr.slice(0, -1)removes the last one
arr.shift()arr.slice(1)removes the first one
arr.splice(i, 1)arr.toSpliced(i, 1)removes at position i
arr.splice(i, 0, x)arr.toSpliced(i, 0, x)inserts at position i
arr[i] = xarr.with(i, x)replaces at position i
arr.sort(fn)arr.toSorted(fn)sorts
arr.reverse()arr.toReversed()reverses the order
const original = [3, 1, 2]
const sorted = original.sort((a, b) => a - b)
sorted // [1, 2, 3]
original // [1, 2, 3] 😱 the original changed too – sorted and original are THE SAME array
const original2 = [3, 1, 2]
const sorted2 = original2.toSorted((a, b) => a - b)
sorted2 // [1, 2, 3]
original2 // [3, 1, 2] ✅

Check yourself

Quiz What does [1, 2, 3].reduce((acc, n) => acc + n, 10) return?
Quiz What does [10, 9, 1].toSorted() return?
Quiz How do you get an array of categories without repeats?
Quiz const b = a.sort(). What's true?

Warm-up

Challenge

Sum and sort

Warm-up

Add up the numbers in an array and sort them from the smallest. The arrays are frozen, so sort won't work.

Task (the same text is in the comment at the top of the file)
CHALLENGE: Sum and sort

1. total([4, 1, 5]) should return 10. Use reduce with the initial value 0.
2. sortAsc([4, 1, 5]) should return [1, 4, 5]. Use toSorted((a, b) => a - b).

Loading the interactive part…

Challenges

The first challenge combines everything from the array lessons on real product data. The second one practises reduce, Set, sorting and flatMap – and the cart is frozen, so sort won't work.

Challenge

Shop statistics

Easy

The statistics table is finished, it just contains made-up numbers. Replace them with calculations from the products array – each value as a single expression.

Task (the same text is in the comment at the top of the file)
CHALLENGE: Shop statistics (array methods)

Data: the `products` array from src/course/fakeApi.en.ts.
Every product looks like this: { id: 1, name: 'Apple', category: 'fruit', price: 12, inStock: true }

The table is finished, only all the values are "hard-coded" for now. Replace each of them
with a calculation from the `products` array – always as a single expression (method chaining is fine).

1. `inStockCount` – how many products are in stock (filter + length).
2. `stockValue` – the sum of the prices of products in stock (filter + reduce, initial value 0).
3. `hasDairy` – is there at least one product from the 'dairy' category? (some)
4. `allCheap` – do all products cost less than $50? (every)
5. `product8` – the name of the product with id 8 (find; find can return undefined → use ?.name)
6. `cheapest` – the name of the cheapest product (toSorted by price and take the first item).
7. `fruitNames` – the fruit names sorted alphabetically and joined with a comma:
   filter → map → toSorted((a, b) => a.localeCompare(b, 'en')) → join(', ')

You must not change the `products` array while doing it (no sort, push, splice…).

Loading the interactive part…

Challenge

Cart report

Medium Bonus challenge

Seven functions that calculate summaries from a cart: the total price, counts, prices per category, unique values and a ranking of the most expensive items.

🔒 Bonus challenge

Medium and hard challenges are a bonus for signed-in readers. Signing up is free – just an e-mail, no password.

How we handle your data

Summary

  • reduce = a loop with a memory: the function gets the accumulator and an item and returns a new accumulator. Always give an initial value and always return the accumulator.
  • The accumulator can be a number (a sum), an object (counts, a dictionary by id) or anything else.
  • Object.groupBy splits an array into groups, Math.max(...arr) finds the maximum of numbers.
  • Sorting: numbers (a, b) => a - b, text a.localeCompare(b, 'en'), several criteria with ||. Without a function it sorts as text.
  • slice cuts out, spread joins, flatMap flattens, [...new Set(arr)] removes duplicates.
  • push, splice, sort, reverse change the array. Safe replacements: spread, toSpliced, with, toSorted, toReversed.
🧩 Where do the things not defined here come from (Checks, deepFreeze, equal, Check)
Checks, deepFreeze, equal, Check – from the file src/course/checks.tsx. The course helper for function-style challenges: runs the tests and shows ✅/❌. You don't need to know how it works inside – it just tells you which steps are done.
/** One test: a description and a function returning true (passed) or false. It may be async. */
export type Check = [name: string, test: () => boolean | Promise<boolean>]
/**
* Runs the tests of a challenge and shows ✅ or ❌ for each.
* An error thrown in a test (e.g. when trying to change a frozen object) is shown under the test.
*/
export function Checks({ tests }: { tests: Check[] }) {
const t = useT()
const [results, setResults] = useState<Result[]>(() => tests.map(() => ({ status: 'pending' })))
useEffect(() => {
let ignore = false
tests.forEach(([, test], i) => {
Promise.resolve()
.then(test)
.then(
(ok): Result => (ok ? { status: 'ok' } : { status: 'fail' }),
(e): Result => ({ status: 'fail', error: e instanceof Error ? e.message : String(e) }),
)
.then((result) => {
if (ignore) return
setResults((prev) => prev.map((r, idx) => (idx === i ? result : r)))
})
})
return () => {
ignore = true
}
}, [tests])
const passed = results.filter((r) => r.status === 'ok').length
return (
<div className="stack">
<strong className={passed === tests.length ? 'ok' : undefined}>
{t.checksPassed(passed, tests.length)}
</strong>
<ul className="list">
{tests.map(([name], i) => {
const r = results[i] ?? { status: 'pending' }
return (
<li key={name}>
{r.status === 'pending' ? '⏳' : r.status === 'ok' ? '✅' : '❌'} {name}
{r.status === 'fail' && r.error && (
<div className="bad muted">
{t.checksError} {r.error}
</div>
)}
</li>
)
})}
</ul>
</div>
)
}
/** Freezes an object and all nested objects and arrays – any attempt to change them throws. */
export function deepFreeze<T>(value: T): T {
if (value && typeof value === 'object') {
Object.values(value).forEach(deepFreeze)
Object.freeze(value)
}
return value
}
/** Compares two values by content (arrays and objects item by item), not by reference. */
export function equal(a: unknown, b: unknown): boolean {
return JSON.stringify(a) === JSON.stringify(b)
}
🧩 Where do the things not defined here come from (products)
products – from the file src/course/fakeApi.en.ts. The course fake API: data and functions pretending to be a server (with a delay, sometimes even with an error). In a real app you would call fetch() here.
export interface Product {
id: number
name: string
category: 'fruit' | 'vegetables' | 'bakery' | 'dairy'
price: number
inStock: boolean
}
export const products: Product[] = [
{ id: 1, name: 'Apple', category: 'fruit', price: 12, inStock: true },
{ id: 2, name: 'Banana', category: 'fruit', price: 8, inStock: true },
{ id: 3, name: 'Pear', category: 'fruit', price: 15, inStock: false },
{ id: 4, name: 'Carrot', category: 'vegetables', price: 6, inStock: true },
{ id: 5, name: 'Tomato', category: 'vegetables', price: 9, inStock: true },
{ id: 6, name: 'Cucumber', category: 'vegetables', price: 19, inStock: false },
{ id: 7, name: 'Bread roll', category: 'bakery', price: 3, inStock: true },
{ id: 8, name: 'Bread', category: 'bakery', price: 45, inStock: true },
{ id: 9, name: 'Milk', category: 'dairy', price: 22, inStock: true },
{ id: 10, name: 'Cheddar', category: 'dairy', price: 39, inStock: true },
{ id: 11, name: 'Yogurt', category: 'dairy', price: 14, inStock: false },
{ id: 12, name: 'Orange', category: 'fruit', price: 11, inStock: true },
]
CSS classes like row, stack, card, list, btn, input or muted are the course's ready-made styles in src/styles.css (row = side by side, stack = stacked, card = bordered box, muted = grey text).