Knowing the individual array methods is not enough โ mastering arrays means knowing which pattern to apply in each situation. This lesson covers the practical patterns that appear in real JavaScript codebases every day: immutable array updates for state management, working with Sets for unique collections, converting between arrays and other data structures, paginating and chunking arrays, and the performance considerations that matter when arrays get large. This is the lesson that turns array knowledge into array fluency.
Immutable Array Update Patterns
| Operation | Mutable (avoid in state) | Immutable (preferred) |
|---|---|---|
| Add to end | arr.push(item) |
[...arr, item] |
| Add to start | arr.unshift(item) |
[item, ...arr] |
| Remove by index | arr.splice(i, 1) |
arr.filter((_, idx) => idx !== i) |
| Remove by value | arr.splice(arr.indexOf(v), 1) |
arr.filter(x => x !== v) |
| Update at index | arr[i] = newVal |
arr.map((x, idx) => idx === i ? newVal : x) |
| Insert at index | arr.splice(i, 0, item) |
[...arr.slice(0,i), item, ...arr.slice(i)] |
Array and Set Interoperability
| Operation | Code | Result |
|---|---|---|
| Array โ Set (deduplicate) | new Set(arr) |
Unique values, insertion order |
| Set โ Array | [...set] or Array.from(set) |
Array of unique values |
| Unique values from array | [...new Set(arr)] |
Deduped array |
| Intersection (A โฉ B) | a.filter(x => setB.has(x)) |
Values in both arrays |
| Difference (A โ B) | a.filter(x => !setB.has(x)) |
Values in A but not B |
| Union (A โช B) | [...new Set([...a, ...b])] |
All unique values from both |
Array Utility Patterns
| Pattern | Code | Use Case |
|---|---|---|
| Chunk array | Array.from({length:Math.ceil(n/size)},(_,i)=>arr.slice(i*size,i*size+size)) |
Pagination, batch processing |
| Zip two arrays | a.map((v,i) => [v, b[i]]) |
Pair elements by index |
| Transpose matrix | matrix[0].map((_, i) => matrix.map(row => row[i])) |
Rotate 2D grid |
| Shuffle array | [...arr].sort(() => Math.random() - 0.5) |
Randomise order (not cryptographic) |
| Count occurrences | arr.reduce((c,x) => ({...c,[x]:(c[x]??0)+1}),{}) |
Frequency map |
| Moving window | Array.from({length:n-k+1},(_,i)=>arr.slice(i,i+k)) |
Sliding window of size k |
filter().map().reduce()) will be slower than a single reduce that does all three in one pass. For small arrays, readability should win over micro-optimisation โ but at scale, profiling matters.Set for membership testing instead of includes() on large arrays. arr.includes(val) is O(n) โ it scans the whole array. set.has(val) is O(1) โ instant lookup regardless of size. When you are checking membership in a loop, convert to a Set once: const lookup = new Set(arr), then use lookup.has(val) inside the loop.Array.from({length: n}) creates a sparse array โ all slots are undefined. Most array methods skip sparse slots, producing unexpected results. Always initialise with a mapping function: Array.from({length: n}, (_, i) => i) to get a dense, fully initialised array that all methods treat consistently.Basic Example
// โโ Immutable state updates (React-style) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let todos = [
{ id: 1, text: 'Learn JavaScript', done: false },
{ id: 2, text: 'Build a project', done: false },
{ id: 3, text: 'Deploy to Vercel', done: false },
];
// Add
const afterAdd = [...todos, { id: 4, text: 'Write tests', done: false }];
// Remove by id
const afterRemove = todos.filter(t => t.id !== 2);
// Toggle done
const afterToggle = todos.map(t => t.id === 1 ? { ...t, done: !t.done } : t);
// Update text
const afterEdit = todos.map(t => t.id === 3 ? { ...t, text: 'Deploy to Fly.io' } : t);
console.log(todos); // original โ unchanged in all cases
console.log(afterAdd.length); // 4
console.log(afterRemove.map(t => t.id)); // [1, 3]
console.log(afterToggle[0].done); // true
// โโ Set operations โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const allTags = ['js', 'css', 'html', 'js', 'react', 'css'];
const uniqueTags = [...new Set(allTags)];
console.log(uniqueTags); // ['js','css','html','react']
const userA = ['js', 'css', 'react'];
const userB = ['js', 'python', 'react', 'sql'];
const setB = new Set(userB);
const common = userA.filter(tag => setB.has(tag)); // intersection
const onlyA = userA.filter(tag => !setB.has(tag)); // difference
const union = [...new Set([...userA, ...userB])]; // union
console.log('Common:', common); // ['js','react']
console.log('Only A:', onlyA); // ['css']
console.log('Union:', union); // ['js','css','react','python','sql']
// โโ Chunk array โ for pagination โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function chunk(arr, size) {
return Array.from(
{ length: Math.ceil(arr.length / size) },
(_, i) => arr.slice(i * size, i * size + size)
);
}
const items = [1,2,3,4,5,6,7,8,9,10];
console.log(chunk(items, 3));
// [[1,2,3],[4,5,6],[7,8,9],[10]]
// โโ Zip two arrays โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const names = ['Alice', 'Bob', 'Carol'];
const scores = [95, 82, 88];
const zipped = names.map((name, i) => ({ name, score: scores[i] }));
console.log(zipped);
// [{name:'Alice',score:95},{name:'Bob',score:82},{name:'Carol',score:88}]
// โโ Frequency map โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const votes = ['red', 'blue', 'red', 'green', 'blue', 'red', 'blue'];
const tally = votes.reduce((acc, v) => {
acc[v] = (acc[v] ?? 0) + 1;
return acc;
}, {});
console.log(tally); // { red: 3, blue: 3, green: 1 }
const winner = Object.entries(tally)
.sort(([, a], [, b]) => b - a)[0][0];
console.log('Winner:', winner); // 'red' or 'blue' (tied โ first alphabetically after sort)
// โโ Performance: Set for O(1) lookup โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const blocklist = ['spam@evil.com', 'bad@actor.com', 'blocked@user.com'];
const blockSet = new Set(blocklist); // build once
function isBlocked(email) {
return blockSet.has(email.toLowerCase()); // O(1)
}
const emails = ['alice@good.com', 'spam@evil.com', 'bob@ok.com'];
const clean = emails.filter(e => !isBlocked(e));
console.log(clean); // ['alice@good.com', 'bob@ok.com']
// โโ Transpose matrix โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const matrix = [[1,2,3],[4,5,6],[7,8,9]];
const transposed = matrix[0].map((_, col) => matrix.map(row => row[col]));
console.log(transposed);
// [[1,4,7],[2,5,8],[3,6,9]]
How It Works
Step 1 โ Immutable Updates Preserve History
React, Redux, and Vue all track state changes by comparing object references. If you mutate an array in place (arr.push(item)), the reference stays the same and the framework cannot detect the change. Returning a new array ([...arr, item]) gives a new reference, triggering a re-render. Immutable updates are also easier to debug โ you can log snapshots at any point without worrying about past values being overwritten.
Step 2 โ Set Has O(1) Lookup
JavaScript’s Set uses a hash-based implementation. Checking membership with set.has(val) is O(1) โ constant time regardless of set size. Array includes() is O(n) โ linear in the size of the array. For membership checks inside loops over large data, this difference is dramatic.
Step 3 โ Set Operations Use filter + has
Converting one array to a Set enables fast membership lookups. Intersection is a.filter(x => setB.has(x)) โ keep elements from A that are in B. Difference is a.filter(x => !setB.has(x)) โ keep elements from A not in B. Both are O(n) with the Set lookup, versus O(nยฒ) if using includes().
Step 4 โ chunk Uses Array.from with Mapping
Array.from({length: n}, fn) creates an array of length n and calls fn for each index. The chunk implementation calculates how many pages are needed (Math.ceil(arr.length / size)), creates that many slots, and fills each with the appropriate slice of the original array.
Step 5 โ Transpose Swaps Rows and Columns
The transpose uses two nested maps: the outer maps over column indices, and the inner maps over rows, picking the value at that column from each row. matrix[0].map((_, col) => ...) iterates over the columns of the first row (determining how many columns there are), and for each column index produces a new row from all original rows at that column.
Real-World Example: Pagination and Batch Processor
// pagination.js
class Paginator {
constructor(items, pageSize = 10) {
this.items = items;
this.pageSize = pageSize;
this.pages = this._chunk(items, pageSize);
}
_chunk(arr, size) {
return Array.from(
{ length: Math.ceil(arr.length / size) },
(_, i) => arr.slice(i * size, i * size + size)
);
}
get totalPages() { return this.pages.length; }
get totalItems() { return this.items.length; }
getPage(n) {
if (n < 1 || n > this.totalPages) return [];
return this.pages[n - 1];
}
getPageInfo(n) {
const start = (n - 1) * this.pageSize + 1;
const end = Math.min(n * this.pageSize, this.totalItems);
return { page: n, total: this.totalPages, start, end, items: this.getPage(n) };
}
}
// Batch processor โ process in chunks to avoid memory spikes
async function processBatch(items, batchSize, processFn) {
const batches = Array.from(
{ length: Math.ceil(items.length / batchSize) },
(_, i) => items.slice(i * batchSize, i * batchSize + batchSize)
);
const results = [];
for (const [i, batch] of batches.entries()) {
console.log(`Processing batch ${i + 1}/${batches.length} (${batch.length} items)`);
const batchResults = await Promise.all(batch.map(processFn));
results.push(...batchResults);
}
return results;
}
// Demo
const allProducts = Array.from({ length: 47 }, (_, i) => ({ id: i + 1, name: `Product ${i + 1}` }));
const paginator = new Paginator(allProducts, 10);
console.log(`Total: ${paginator.totalItems} items, ${paginator.totalPages} pages`);
// Total: 47 items, 5 pages
console.log(paginator.getPageInfo(1));
// { page:1, total:5, start:1, end:10, items:[{id:1,...},...] }
console.log(paginator.getPageInfo(5));
// { page:5, total:5, start:41, end:47, items:[{id:41,...},...] }
Common Mistakes
Mistake 1 โ Mutating state arrays directly
โ Wrong โ direct mutation breaks React state and makes debugging hard:
// In a React component:
state.items.push(newItem);
setState(state); // React sees same reference โ no re-render!
โ Correct โ return new array:
setState({ ...state, items: [...state.items, newItem] });
Mistake 2 โ Using includes() in a loop for large arrays
โ Wrong โ O(nยฒ) total time for n items checked against n-size array:
const result = bigList.filter(item => allowlist.includes(item.id));
โ Correct โ convert to Set once for O(n) total:
const allowed = new Set(allowlist);
const result = bigList.filter(item => allowed.has(item.id));
Mistake 3 โ Creating sparse arrays without fill
โ Wrong โ sparse array โ methods skip empty slots:
const arr = new Array(5);
console.log(arr); // [empty x5]
console.log(arr.map(() => 1)); // [empty x5] โ map skips sparse slots!
โ Correct โ use Array.from with initialiser or fill:
const arr = Array.from({ length: 5 }, () => 0); // [0,0,0,0,0]
// or: new Array(5).fill(0) // [0,0,0,0,0]
Quick Reference
| Pattern | Code |
|---|---|
| Immutable add | [...arr, item] |
| Immutable remove | arr.filter(x => x.id !== id) |
| Immutable update | arr.map(x => x.id === id ? {...x, ...changes} : x) |
| Deduplicate | [...new Set(arr)] |
| Intersection | a.filter(x => new Set(b).has(x)) |
| Chunk | Array.from({length:Math.ceil(n/s)},(_,i)=>arr.slice(i*s,i*s+s)) |
| Frequency map | arr.reduce((c,x)=>({...c,[x]:(c[x]??0)+1}),{}) |
| Fast lookup | const s = new Set(arr); s.has(val) โ O(1) |