Your .sort() is mutating state. JavaScript finally gave you the fix.
The classic array methods — sort, reverse, splice — mutate the original array, silently breaking React state updates and surprising anyone who forgot to spread first. ES2023 added four immutable counterparts: toSorted, toReversed, toSpliced, and with.

Here is a bug I have shipped before. Maybe you have too:
This looks correct. It doesn't work correctly. Array.prototype.sort mutates the array in place and returns the same reference. sorted and tasks point to the same array. React sees the same reference it had before and skips the re-render. The UI doesn't update, or it updates inconsistently — it depends on whether anything else triggered a render cycle.
The fix you've been writing for years:
The defensive spread creates a new array, sort mutates that one, and you get a fresh reference React can track. It works. It's also cargo-cult boilerplate you have to remember to write, and it's easy to forget under pressure.
ES2023 added four methods that make the spread unnecessary.
Array.prototype.toSorted is the non-mutating version of sort. It returns a new sorted array and leaves the original unchanged:
The API is identical to sort. Any comparator that works with sort works with toSorted. The only difference is where the result lives.
toReversed is the same story for reverse:
Array.prototype.reverse is one of the sneakier mutators because it returns this — the same array — making const rev = arr.reverse() look like it produced a copy when it didn't.
Runs right in your browser — poke at it and watch the concept react live.
splice is the most powerful mutator in the array API. It inserts, removes, and replaces elements in place and returns the removed items — not the resulting array. toSpliced flips that contract: it returns the new array with the changes applied and leaves the original unchanged.
The argument signature mirrors splice exactly: toSpliced(start, deleteCount, ...items). For insertions at a specific position, deletions from a slice, or multi-item replacements — it's the same mental model, no mutation.
This one is new in a different way. There was no mutable counterpart to replace because direct assignment (arr[i] = value) already did it. What didn't exist was a way to replace one item at a specific index without touching the original.
The workaround was either map:
Or spread-and-reassign:
Both communicate the intent poorly. with is the named operation:
It also supports negative indices — items.with(-1, 'z') replaces the last element — matching the same convention as Array.prototype.at.
State in React must be treated as immutable. That rule is not a preference — React uses object identity to detect changes, so mutating an existing array and passing the same reference produces no re-render, regardless of whether the contents changed. The same applies to Vue's reactivity system, Zustand stores, Redux reducers, and any library that compares references before scheduling an update.
The immutable array methods give you the correct default:
Less ceremony, clearer intent, and no defensive spread to forget.
All four methods are Baseline 2023: Chrome 110, Firefox 115, Safari 16, Node.js 20. That's roughly every browser released in the last two years. No polyfill needed if your browserslist target is reasonably current; a one-liner shim covers the rest.
Think it clicked? Take the 6-question quiz →
Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.
Search your codebase for ].sort( and [... preceding a sort. Every [...arr].sort(fn) can become arr.toSorted(fn). Every [...arr].reverse() can become arr.toReversed(). Every arr.map((item, i) => i === idx ? … : item) that replaces by index can become arr.with(idx, …).
The defensive spread was never the right abstraction — it was a workaround for methods that should have had immutable variants from the start. They do now. Use them.
Thanks for reading! Let's stay connected:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 💬 Discord — join the frontend best-practices community: discord.gg/d9KRhuAwQ
- 📸 Instagram — frontend best practices, daily: @bestpractice___
- 💼 LinkedIn — linkedin.com/in/parsa-jiravand
- ✉️ Email (work & contract inquiries): bestpractice2026@gmail.com
Originally published on dev.to