Warming up the neural circuits...
By the end of this chapter you will:
.map(), .filter(), and .reduce() fluently without confusiontoSorted() and .with()for loops?Every app deals with lists of data — users, products, messages, prices. Without Array methods, you'd write a manual for loop every time: track a counter, check .length, increment, grab each item. That's error-prone and hard to read.
Array methods let you say what you want instead of how to do it:
prices.map(p => p * 2) — done.Declarative code is shorter, clearer, and eliminates the "off-by-one" bugs that haunt manual loops.
Before diving in, understand these four ideas. They come up in every method.
1. Non-mutating (safe) methods leave the original array alone and return a brand-new array. This is critical in React and Vue — the framework detects changes by checking if the array reference changed. If you mutate in place, the UI won't update.
2. Mutating (in-place) methods change the original array directly. No new array is created. This is fine for local scripts but can cause "ghost bugs" in frameworks.
3. Short-circuiting means the method stops early. If you're searching for one item and .find() matches at position 3, it never checks positions 4, 5, 6... This is a huge performance win on large arrays.
4. Callback function — a function you pass into an Array method. The method calls it once for every item. Example: .map(item => item * 2) — the item => item * 2 part is the callback.
Pick your method by looking at what shape you need:
.map().filter().reduce(), .find(), .some().flatMap()These are the workhorses of JavaScript. They loop through every item and never change the original array.
map() — Transform every itemWhat it does: Calls your callback once for each item and builds a new array from the return values. The new array is always the same length as the original.
Signature: arr.map(callback(currentValue, index, array))
const prices = [100, 200, 300];
const withTax
Rendering diagram…
| Method | Returns | Mutates? | Short-circuits? |
|---|---|---|---|
map | New array (same length) | ❌ No | ❌ No |
filter | New array (subset) | ❌ No | ❌ No |
reduce | Any single value | ❌ No | ❌ No |
forEach | undefined | ❌ No | ❌ No |
find | Item or undefined | ❌ No | ✅ Yes |
findIndex | Number or -1 | ❌ No | ✅ Yes |
some | Boolean | ❌ No | ✅ Yes |
every | Boolean | ❌ No | ✅ Yes |
includes | Boolean | ❌ No | ✅ Yes |
sort | Same array (mutated) | ✅ Yes |
| Mistake | What actually happens | Fix |
|---|---|---|
[1, 10, 2].sort() without compare fn | [1, 10, 2] — sorts as strings, "10" < "2" | arr.sort((a, b) => a - b) |
.find() result used without null check | TypeError: Cannot read properties of undefined | const item = arr.find(...); item?.name or check if (item) |
.map() with no return value | Returns [undefined, undefined, ...] | Use .forEach() for side effects, or add return |
.reduce() on empty array, no initial value | TypeError: Reduce of empty array with no initial value | Always provide the initial value: reduce(fn, 0) |
|
name property from each product into a new array of strings.[{ id: 1, name: 'Phone', price: 500 }, { id: 2, name: 'Tablet', price: 300 }]['Phone', 'Tablet']qty > 0).[{ id: 1, name: 'Widget', qty: 5 }, { id: 2, name: 'Gadget', qty: 0 }][{ id: 1, name: 'Widget', qty: 5 }].filter() then .reduce().[{ id: 1, price: 10, inStock: true }, { id: 2, price: 20, inStock: false }, { id: 3, price: 30, inStock: true }]40age ascending without mutating the original array.[{ id: 1, name: 'Alice', age: 30 }, { id: 2, name: 'Bob', age: 25 }][{ Bob, 25 }, { Alice, 30 }] — original array unchangedcategory property using .reduce(). The result should be an object where each key is a category name.[{ id: 1, name: 'Shirt', category: 'clothing' }, { id: 2, name: 'Laptop', category: 'tech' }, { id: 3, name: 'Pants', category: 'clothing' }]{ clothing: [Shirt, Pants], tech: [Laptop] }tasks array in React state. Write the three state update functions: addTask(title), toggleDone(id), and deleteTask(id). None of them should mutate the original state.tasks = [{ id: 1, title: 'Buy milk', done: false }]setTasks with a new arrayWhat does [1, 2, 3].map(n => n * 2) return?
| Property | Value |
|---|---|
| Returns | New array, same length as original |
| Mutates original | ❌ No |
| Use when | You want to transform every (e.g., format, calculate, extract a property) |
Real-world example:
// Extract display names from an API response
const users = [
{ id: 1, firstName: 'Alice', lastName: 'Smith' },
{ id: 2, firstName: 'Bob', lastName: 'Jones' },
];
const displayNames = users.map(u => `${u.firstName} ${u.lastName}`);
// ['Alice Smith', 'Bob Jones']Common mistake: Using .map() when you don't need the return value. If you're just doing a side effect (logging, updates), use .forEach() instead. A .map() that doesn't return anything produces [undefined, undefined, ...].
filter() — Keep only matching itemsWhat it does: Calls your callback for every item. If the callback returns a truthy value, the item is kept. Returns a new array that is the same length or shorter.
Signature: arr.filter(callback(currentValue, index, array))
const stock = [
{ id: 1, name: 'Widget', qty: 5 },
{ id: 2, name: 'Gadget', qty: 0 },
{ id: 3, name: 'Donut', qty: 12 },
];
const available = stock.filter(item => item.qty > 0);
// [{ id: 1, name: 'Widget', qty: 5 }, { id: 3, name: 'Donut', qty: 12 }]| Property | Value |
|---|---|
| Returns | New array, same length or shorter |
| Mutates original | ❌ No |
| Use when | You want a subset of items that pass a test |
Real-world example:
// Show only active admin users
const users = [
{ id: 1, name: 'Alice', role: 'admin', active: true },
{ id: 2, name: 'Bob', role: 'viewer', active: true },
{ id: 3, name: 'Carol', role: 'admin', active: false },
];
const activeAdmins = users.filter(u => u.role === 'admin' && u.active);
// [{ id: 1, name: 'Alice', ... }]reduce() — Collapse to a single valueWhat it does: Runs your callback on each item, passing the result of the previous call as the accumulator. Returns one final value — which can be a number, string, object, or even another array.
Signature: arr.reduce(callback(accumulator, currentValue, index, array), initialValue)
const cart = [10, 20, 30];
const total = cart.reduce((acc, price) => acc + price, 0);
// 60Step-by-step walkthrough:
acc = 0acc = 0 + 10 = 10acc = 10 + 20 = 30acc = 30 + 30 = 6060| Property | Value |
|---|---|
| Returns | Single value of any type |
| Mutates original | ❌ No |
| Use when | You want to sum, count, group, or transform an array into something entirely different |
[].reduce((acc, x) => acc + x) crashes with TypeError: Reduce of empty array with no initial value. Always write reduce((acc, x) => ..., 0) (or [], {} etc.) as the second argument.
Real-world example — group by category:
const products = [
{ id: 1, name: 'Shirt', category: 'clothing' },
{ id: 2, name: 'Laptop', category: 'tech' },
{ id: 3, name: 'Pants', category: 'clothing' },
];
const grouped = products.reduce((acc, product) => {
(acc[product.category] ??= []).push(product);
return acc;
}, {});
// { clothing: [Shirt, Pants], tech: [Laptop] }forEach() — Loop with no return valueWhat it does: Calls your callback for every item. Unlike .map(), it always returns undefined. Use it purely for side effects.
const ids = [1, 2, 3];
ids.forEach(id => {
console.log('Processing:', id);
// Send to analytics, update DOM, etc.
});
// Returns: undefined| Property | Value |
|---|---|
| Returns | undefined (always) |
| Mutates original | ❌ No (but your callback can) |
| Use when | You need to loop and perform side effects, not build a new array |
flatMap() — Transform + flatten in one stepWhat it does: Like .map() but also flattens one level of nesting. Perfect when your callback returns arrays and you want a single flat result.
const sentences = ['hello world', 'foo bar'];
const words = sentences.flatMap(s => s.split(' '));
// ['hello', 'world', 'foo', 'bar']
// Without flatMap: [['hello', 'world'], ['foo', 'bar']]| Property | Value |
|---|---|
| Returns | New array, flattened one level |
| Mutates original | ❌ No |
| Use when | Your .map() callback returns arrays and you want a flat result |
These methods stop early the moment they have an answer. On a 1 million-item array, if .find() matches at item #3, it never checks the other 999,997.
find() — Get the first matching itemWhat it does: Returns the first item where your callback returns truthy. Returns undefined if nothing matches.
const users = [
{ id: 1, name: 'Alice', active: false },
{ id: 2, name: 'Bob', active: true },
{ id: 3, name: 'Carol', active: true },
];
const firstActive = users.find(u => u.active);
// { id: 2, name: 'Bob', active: true }
// Stops at id:2, never checks id:3 ✅| Property | Value |
|---|---|
| Returns | The item itself, or undefined |
| Mutates original | ❌ No |
| Short-circuits | ✅ Yes — stops at first match |
| Use when | You need one specific record from a list |
const item = arr.find(...); item.name — if nothing matches, item is undefined and .name throws TypeError: Cannot read properties of undefined. Always write item?.name or check if (item) first.
findIndex() — Get the position of the first matchLike .find() but returns the (position number) instead of the item. Returns -1 if not found.
const todos = [
{ id: 10, done: false },
{ id: 11, done: true },
];
const idx = todos.findIndex(t => t.id === 11)
some() — Does ANY item pass?Returns true the moment any callback returns truthy. Returns false if all fail (or array is empty).
const orders = [
{ id: 1, shipped: false },
{ id: 2, shipped: true },
];
const hasShipped = orders.some(o => o.shipped);
//every() — Do ALL items pass?Returns false the moment any callback returns falsy. Returns true only if all pass.
const payments = [
{ id: 1, confirmed: true },
{ id: 2, confirmed: false },
];
const allConfirmed = payments.every(p => p.confirmed);
includes() — Does a value exist?Checks if an array contains a specific primitive value. Returns a boolean.
const roles = ['admin', 'editor', 'viewer'];
roles.includes('admin'); // true
roles.includes('owner'); // falseLimitation: Uses strict equality (===). Can't search for objects by content — only by reference.
const items = [{ id: 1 }];
items.includes({ id: 1 }); // false — different object in memory
// Use .some() for objects: items.some(i => i.id === 1)indexOf() — Get the position of a valueReturns the index of the first exact match, or -1 if not found. For objects, prefer .findIndex().
const colors = ['red', 'green', 'blue', 'green'];
colors.indexOf('green'); // 1 (first occurrence)
colors.lastIndexOf('green');
sort(), reverse(), splice(), fill(), and copyWithin() all mutate in place. In React or Vue , never call these directly on state. Use the spread trick ([...arr].sort()) or the new ES2023 immutable alternatives below.
sort() — Sort in placeThe trap: By default, .sort() converts items to strings and compares their Unicode values. [1, 10, 2].sort() gives [1, 10, 2] because the string "10" comes before "2".
The fix: Always provide a compare function for numbers.
const nums = [1, 10, 2, 20];
nums.sort((a, b) => a - b); // [1, 2, 10, 20] ascending
nums.sort((a,
| Property | Value |
|---|---|
| Returns | The same array (mutated) |
| Mutates original | ✅ Yes |
| Use when | You need to sort in place (or use toSorted() for a safe copy) |
splice() vs slice() — The classic confusionsplice(start, deleteCount, ...items) — mutates the original array (add/remove/replace).
const letters = ['a', 'b', 'c', 'd'];
// Remove 1 item at index 1
const removed = letters.splice(1, 1);
// removed = ['b'], letters is now ['a', 'c', 'd']
slice(start, end?) — non-mutating, returns a new array.
const nums = [10, 20, 30, 40, 50];
const middle = nums.slice(1, 4); // [20, 30, 40]
const lastTwo = nums.slice(-2);
Memory trick: sPlice changes the original (like splicing wires). sLice cuts a copy (like slicing bread, the loaf stays).
concat() — Merge arrays (non-mutating)Returns a new array combining the original with one or more arrays or values.
const a = [1, 2];
const b = [3, 4];
const merged = a.concat(b, [5, 6]);
// [1, 2, 3, 4, 5, 6]
// Modern equivalent: [...a, ...b, 5, 6]flat() — Flatten nested arraysconst nested = [1, [2, [3, [4]]]];
nested.flat(); // [1, 2, [3, [4]]] — 1 level
nested.flat(2); // [1, 2, 3, [4]] — 2 levels
nested.flat(InfinityThe JS ecosystem is moving toward functional purity — never mutating data. ES2023 added immutable versions of the most common mutating methods.
toSorted() — Sort without mutatingconst nums = [3, 1, 4, 1, 5];
const sorted = nums.toSorted((a, b) => a - b);
// sorted: [1, 1, 3, 4, 5]
// nums: [3, 1, 4, 1, 5] ← untouched ✅Before this existed, the workaround was [...nums].sort(...). Now it's built in.
toReversed() — Reverse without mutatingconst items = ['a', 'b', 'c'];
const rev = items.toReversed();
// rev: ['c', 'b', 'a']
// items: ['a', 'b', 'c'] ← untouched ✅with(index, value) — Update one item by indexThe old way to update one item immutably was: const next = [...arr]; next[1] = newVal;. Now:
const scores = [10, 20, 30, 40];
const updated = scores.with(2, 99);
// updated: [10, 20, 99, 40]
// scores: [10, 20, 30, 40] ← untouched ✅
// Perfect for React state updates:
setScores(prevjoin() — Convert array to stringconst words = ['Hello', 'world', 'today'];
words.join(' '); // 'Hello world today'
words.join(', '); // 'Hello, world, today'
words.join(''); // 'Helloworldtoday'push(), pop(), shift(), unshift() — Mutating add/removeconst stack = [1, 2, 3];
stack.push(4); // [1, 2, 3, 4] — add to end
stack.pop(); // returns 4, stack is [1, 2, 3]
stack.unshift(0); // [0, 1, 2, 3] — add to start
All four mutate the original array.
Array.from() — Create arrays from anything iterable// From a string
Array.from('hello'); // ['h', 'e', 'l', 'l', 'o']
// From a NodeList
Array.from(document.querySelectorAll('.btn')); // real Array
// With a mapping function (like a range generator)
Array.isArray() — Type checkArray.isArray([1, 2, 3]); // true
Array.isArray('hello'); // false
Array.isArray({ length: 3 }); // falseentries(), keys(), values() — Iterate with indexconst fruits = ['apple', 'banana', 'cherry'];
for (const [index, value] of fruits.entries()) {
console.log(index, value)
| ❌ No |
splice | Removed items array | ✅ Yes | ❌ No |
slice | New array (copy) | ❌ No | ❌ No |
toSorted | New sorted array | ❌ No | ❌ No |
with | New array (one item changed) | ❌ No | ❌ No |
In React, state is read-only. To update a list you must create a new array reference — the only way React's reconciler knows something changed.
function TaskManager({ initialTasks }) {
const [tasks, setTasks] = useState(initialTasks);
const [filter, setFilter] = useState('all');
// Derive the visible list — never mutate tasks directly
const visible = tasks.filter(t =>
filter === 'all' ? true : t.status === filter
);
// Add a task
const addTask = (title) =>
setTasks(prev => [...prev, { id: Date.now(), title, status: 'open' }]);
// Mark done — .map() creates a new array
const markDone = (id) =>
setTasks(prev =>
prev.map(t => t.id === id ? { ...t, status: 'done' } : t)
);
// Delete — .filter() creates a new array
const deleteTask = (id) =>
setTasks(prev => prev.filter(t => t.id !== id));
return (
<ul>
{visible.map(task => (
<li key={task.id}>
{task.title}
<button onClick={() => markDone(task.id)}>Done</button>
<button onClick={() => deleteTask(task.id)}>Delete</button>
</li>
))}
</ul>
);
}Mastery tip: Never use the array index as key if the array can be filtered or sorted. Use a stable id instead.
Always false — objects compared by reference, not content |
Use arr.some(i => i.id === 1) |
Calling .sort() directly on React state | Mutates state in place, UI may not re-render | Use toSorted() or [...state].sort(...) |
forEach instead of map when building a list | forEach returns undefined, breaking the chain | Use .map() when you need the result array |