Home / Articles / Modern JavaScript Array Methods That Replace Common Boilerplate Patterns

This article is published in English.

Modern JavaScript Array Methods That Replace Common Boilerplate Patterns

Learn how newer JavaScript array methods like groupBy, toSorted, with, and findLast replace verbose reduce, spread, and reverse workarounds.

1432 words

Grouping Data Without a Library: Object.groupBy() and Map.groupBy()

For years, splitting an array of items into categories based on a shared property meant writing your own reduce() accumulator or pulling in lodash.groupBy just for that one task.

The ECMAScript spec now ships native grouping through Object.groupBy() and Map.groupBy().

What problem this fixes

When you pull a flat collection of records from an API — transactions, tasks, user entries, whatever — you often need to sort them into buckets before displaying them or running business logic on them.

Before: hand-rolling it with reduce()

const inventory = [
  { name: 'Apples', type: 'fruit', quantity: 10 },
  { name: 'Bananas', type: 'fruit', quantity: 0 },
  { name: 'Carrots', type: 'vegetable', quantity: 14 },
  { name: 'Broccoli', type: 'vegetable', quantity: 5 }
];

const groupedByReduce = inventory.reduce((acc, item) => {
  const key = item.type;
  if (!acc[key]) {
    acc[key] = [];
  }
  acc[key].push(item);
  return acc;
}, {});

After: a clear, purpose-built call to Object.groupBy

const inventory = [
  { name: 'Apples', type: 'fruit', quantity: 10 },
  { name: 'Bananas', type: 'fruit', quantity: 0 },
  { name: 'Carrots', type: 'vegetable', quantity: 14 },
  { name: 'Broccoli', type: 'vegetable', quantity: 5 }
];

const grouped = Object.groupBy(inventory, item => item.type);

/*
Output:
{
  fruit: [
    { name: 'Apples', type: 'fruit', quantity: 10 },
    { name: 'Bananas', type: 'fruit', quantity: 0 }
  ],
  vegetable: [
    { name: 'Carrots', type: 'vegetable', quantity: 14 },
    { name: 'Broccoli', type: 'vegetable', quantity: 5 }
  ]
}
*/

Reaching for Map.groupBy() instead

When your grouping key isn't a plain string or symbol but a richer object or dynamic reference, Map.groupBy() is the better fit:

const vipTier = { tier: 'VIP' };
const standardTier = { tier: 'Standard' };

const customers = [
  { name: 'Alice', plan: vipTier },
  { name: 'Bob', plan: standardTier },
  { name: 'Charlie', plan: vipTier }
];

const groupedByPlan = Map.groupBy(customers, user => user.plan);

console.log(groupedByPlan.get(vipTier));
// Returns Alice and Charlie's records directly keyed by reference

Immutable Transforms You Can Trust: toSorted(), toReversed(), toSpliced()

A classic JavaScript gotcha is that the familiar .sort(), .reverse(), and .splice() all rewrite the array in place rather than returning something new.

In frameworks built around predictable state — React, Redux, Vue, and similar — mutating an array directly tends to produce subtle bugs and rendering that silently falls out of sync.

What problem this fixes

To avoid these traps, developers used to write defensive copies first, such as [...array].sort() or array.slice().reverse(). The new "change array by copy" family of methods removes that boilerplate entirely.

Comparing the two approaches

// --- The Old Mutating Way ---
const scores = [88, 92, 79, 95];
const sortedMutated = scores.sort((a, b) => a - b);

console.log(scores); // [79, 88, 92, 95] -> Original data was destroyed!

// --- The Modern Non-Mutating Way ---
const originalScores = [88, 92, 79, 95];
const cleanSorted = originalScores.toSorted((a, b) => a - b);

console.log(originalScores); // [88, 92, 79, 95] -> Unchanged
console.log(cleanSorted);    // [79, 88, 92, 95] -> Fresh array

Swapping splice() for toSpliced()

toSpliced() lets you remove, insert, or replace entries at any position while leaving the original array untouched:

const tabs = ['Home', 'About', 'Pricing', 'Contact'];

// Remove 'Pricing' (index 2) and insert 'Services' & 'Blog'
const updatedTabs = tabs.toSpliced(2, 1, 'Services', 'Blog');

console.log(tabs);        // ['Home', 'About', 'Pricing', 'Contact']
console.log(updatedTabs); // ['Home', 'About', 'Services', 'Blog', 'Contact']

Updating One Element Immutably: with()

Say you want to change the value at a given index without touching the source array. The usual pattern is to spread the array into a copy, overwrite the index, and return the result. with() collapses all of that into one clear expression.

What problem this fixes

State updates commonly need to swap out a single element by position while keeping the surrounding array immutable.

Comparing the two approaches

const months = ['Jan', 'Mar', 'Mar', 'Apr'];

// Old way: Spread and mutate copy
const fixedMonthsOld = [...months];
fixedMonthsOld[1] = 'Feb';

// Modern way: array.with(index, value)
const fixedMonthsNew = months.with(1, 'Feb');

console.log(months);         // ['Jan', 'Mar', 'Mar', 'Apr']
console.log(fixedMonthsNew);  // ['Jan', 'Feb', 'Mar', 'Apr']

with() also supports negative indices, so replacing an element counted from the end is just as simple:

const tags = ['v1.0.0', 'v1.1.0', 'v1.2.0-beta'];
const stableTags = tags.with(-1, 'v1.2.0');

console.log(stableTags); // ['v1.0.0', 'v1.1.0', 'v1.2.0']

Searching Backwards Efficiently: findLast() and findLastIndex()

Locating an element with .find() works fine when scanning forward from the start, but it always begins at index 0. When what you actually need is the last match, the common workaround was reversing the array first — an approach that wastes time and risks mutating data you didn't mean to touch.

What problem this fixes

Data like log entries, undo history, activity feeds, and transaction records is naturally ordered by time, and you often want the most recent entry that matches some condition.

Comparing the two approaches

const logs = [
  { id: 1, action: 'LOGIN', timestamp: 1000 },
  { id: 2, action: 'UPLOAD', timestamp: 1010 },
  { id: 3, action: 'ERROR', timestamp: 1020 },
  { id: 4, action: 'UPLOAD', timestamp: 1030 },
  { id: 5, action: 'LOGOUT', timestamp: 1040 }
];

// Old way: Reversing creates overhead and array allocation
const lastUploadOld = [...logs].reverse().find(log => log.action === 'UPLOAD');

// Modern way: findLast iterates backwards directly
const lastUpload = logs.findLast(log => log.action === 'UPLOAD');
const lastUploadIdx = logs.findLastIndex(log => log.action === 'UPLOAD');

console.log(lastUpload);    // { id: 4, action: 'UPLOAD', timestamp: 1030 }
console.log(lastUploadIdx); // 3

Because this scans from the tail and stops the moment it finds a match, it runs in O(k) time, where k is the number of steps back to the match — no need to copy or reverse the whole array first.

5. Doing Map and Filter Together: flatMap()

It's common to chain .map() with .filter() or .flat() when each source item might expand into zero, one, or several results. flatMap() collapses that chain into one pass.

What it solves: it avoids building throwaway intermediate arrays when you need to reshape nested data or filter and transform in the same step.

Example: flattening a nested list of relationships

const authors = [
  { name: 'Author A', books: ['Book 1', 'Book 2'] },
  { name: 'Author B', books: ['Book 3'] },
  { name: 'Author C', books: [] }
];

// Map + Flat produces 2 array allocations
const booksNested = authors.map(a => a.books).flat();

// flatMap runs the map and flattens depth-1 in one pass
const allBooks = authors.flatMap(a => a.books);
console.log(allBooks); // ['Book 1', 'Book 2', 'Book 3']

Example: transforming and filtering together

You can discard an item by returning an empty array and keep it by returning a single-element array:

const rawInputs = ['42', 'invalid', '100', 'undefined', '256'];

const validNumbers = rawInputs.flatMap(input => {
  const num = parseInt(input, 10);
  return Number.isNaN(num) ? [] : [num];
});

console.log(validNumbers); // [42, 100, 256]

6. Cleaner Element Access: at()

Grabbing the last item of an array has traditionally meant writing array[array.length - 1], which is awkward and easy to get wrong.

at() borrows the negative-index convention familiar from Python, letting you count backward from the end of arrays and strings alike.

Before and after

const queue = ['Alice', 'Bob', 'Charlie', 'Dana'];

// Old way
const lastPersonOld = queue[queue.length - 1];
const secondLastOld = queue[queue.length - 2];

// Modern way
const lastPerson = queue.at(-1);    // 'Dana'
const secondLast = queue.at(-2);    // 'Charlie'

The result is shorter, less prone to off-by-one mistakes in computed expressions, and it plays nicely with chained method calls since you don't need to store the array in a temporary variable first.

When to reach for it

Modernization Checklist

  • Review how you manage state: swap .sort() and .reverse() for .toSorted() and .toReversed() to avoid silent mutation bugs.
  • Cut boilerplate reducers: use Object.groupBy() instead of hand-rolled accumulator objects.
  • Speed up lookups: replace reverse-then-find patterns with findLast() for time-ordered data.
  • Simplify everyday syntax: rely on .at(-1) and .with(index, value) instead of manual length arithmetic and defensive array copies.