Skip to content
Carlo Fontanos
Carlo Fontanos

Carlo Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

toSorted() and Friends: Arrays That Stop Mutating Under You

C
Carlo Fontanos
· 2 min read

The oldest trap in JavaScript: sort() sorts in place. Sort the props you received, the array in a parent component, the cached response - and you've mutated shared state that three other places were relying on. I've debugged that exact bug in production more times than I'll admit.

// The old defensive idiom
const sorted = [...items].sort((a, b) => a.price - b.price);

// ES2023: says what it means
const sorted = items.toSorted((a, b) => a.price - b.price);

Four methods arrived together, each a non-mutating twin of a classic offender:

const byPrice  = products.toSorted((a, b) => a.price - b.price);
const newest   = posts.toReversed();
const trimmed  = items.toSpliced(2, 1);            // copy with index 2 removed
const updated  = rows.with(3, { ...rows[3], done: true });   // copy with index 3 replaced

with() is the sleeper

Updating one item at a known index used to be the ugliest line in state updates:

// before
const next = items.map((item, i) => i === index ? newValue : item);

// after
const next = items.with(index, newValue);

It reads like array indexing, supports negative indices (list.with(-1, x) replaces the last element), and throws RangeError on out-of-bounds instead of silently creating sparse weirdness.

Why this matters beyond taste

  • Framework state: React and friends detect changes by reference. Mutating methods keep the same reference, so the UI doesn't update - the single most common "why won't it re-render" cause. The to*() methods return new arrays by construction.
  • The spread idiom lies a little: [...arr].sort() works but buries the intent in punctuation, and people routinely forget the copy step under deadline. A method named toSorted can't be mis-written into its mutating cousin.
  • Shallow, like everything else: the array is new, the objects inside are shared. For replacing an object's field, combine with spread as in the with() example - or reach for structuredClone when you genuinely need a deep copy.

Support and the memory footnote

All four landed everywhere in 2023: Chrome 110+, Firefox 115+, Safari 16.4+, Node 20+. Each call allocates a new array - irrelevant at UI scale (hundreds of items), worth thinking about in a hot loop over hundreds of thousands, where in-place sort of a local copy you own is still the right tool. Own the array? Mutate freely. Received it from anywhere else? to*() by default.

Honorary mention to their older siblings from the same cleanup effort: at(-1) for the last element and findLast()/findLastIndex() for searching from the end - all universally supported and all quietly deleting helper functions from your utils file.

C
Written by Carlo Fontanos

Full-stack web developer sharing practical tutorials and building tools that ship.

Got something on your mind?

My inbox is open - no forms disappearing into the void here.

  • Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
  • Hire me for a project Have something custom in mind? Let's talk scope and timelines.
  • Product support Bought something here? I'll help you get it running.

I usually reply within 1-2 business days.

Your details are only used to reply to you.

Keep reading