← blog
JavaScriptJuly 15, 2026 · 4 min read

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.

Parsa Jiravand · Frontend engineer · building bestpractic
Your `.sort()` is mutating state. JavaScript finally gave you the fix.

Here is a bug I have shipped before. Maybe you have too:

JavaScript
1
2
3
4
5
6
const [tasks, setTasks] = useState(initialTasks); function sortByPriority() { const sorted = tasks.sort((a, b) => b.priority - a.priority); setTasks(sorted); }

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:

JavaScript
const sorted = [...tasks].sort((a, b) => b.priority - a.priority);

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:

JavaScript
1
2
3
4
5
6
7
8
9
10
const tasks = [ { id: 1, priority: 2, title: 'Review PR' }, { id: 2, priority: 5, title: 'Fix regression' }, { id: 3, priority: 1, title: 'Update docs' }, ]; const sorted = tasks.toSorted((a, b) => b.priority - a.priority); console.log(sorted[0].title); // 'Fix regression' console.log(tasks[0].title); // 'Review PR' — untouched

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:

JavaScript
1
2
3
4
5
const steps = ['write test', 'make it pass', 'refactor']; const reversed = steps.toReversed(); console.log(reversed); // ['refactor', 'make it pass', 'write test'] console.log(steps[0]); // 'write test' — original intact

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.

JavaScript
1
2
3
4
5
6
7
const items = ['a', 'b', 'c', 'd']; // Remove 1 item at index 1, insert 'x' and 'y' const updated = items.toSpliced(1, 1, 'x', 'y'); console.log(updated); // ['a', 'x', 'y', 'c', 'd'] console.log(items); // ['a', 'b', 'c', 'd'] — untouched

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:

JavaScript
const updated = items.map((item, i) => i === targetIndex ? newValue : item);

Or spread-and-reassign:

JavaScript
const updated = [...items.slice(0, i), newValue, ...items.slice(i + 1)];

Both communicate the intent poorly. with is the named operation:

JavaScript
1
2
3
4
5
const items = ['a', 'b', 'c', 'd']; const updated = items.with(1, 'z'); console.log(updated); // ['a', 'z', 'c', 'd'] console.log(items); // ['a', 'b', 'c', 'd']

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:

JavaScript
1
2
3
4
5
// Before setTasks(prev => [...prev].sort((a, b) => b.priority - a.priority)); // After setTasks(prev => prev.toSorted((a, b) => b.priority - a.priority));
JavaScript
1
2
3
4
5
// Before — delete an item setItems(prev => prev.filter((_, i) => i !== indexToRemove)); // After — splice idiom setItems(prev => prev.toSpliced(indexToRemove, 1));
JavaScript
1
2
3
4
5
// Before — update one item setItems(prev => prev.map((item, i) => i === idx ? { ...item, done: true } : item)); // After setItems(prev => prev.with(idx, { ...prev[idx], done: true }));

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.

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:

Originally published on dev.to