Array Patterns and Best Practices

โ–ถ Try It Yourself

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
Note: When working with large arrays (10,000+ elements), be aware of the performance characteristics of each method. Methods that iterate the array multiple times (chained 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.
Tip: Use a 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.
Warning: 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]

▶ Try It Yourself

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)

🧠 Test Yourself

You need to check if a user’s ID appears in a blocklist of 100,000 IDs, inside a loop of 50,000 users. What is the most performant approach?





โ–ถ Try It Yourself