Finally, safe array methods in JavaScript
Many developers pause before using .sort(), .reverse(), or .splice() for a good reason: they mutate the original array. That one side effect causes subtle, hard-to-trace bugs, especially with shared or reactive state. The good news is that we now have non-mutating versions:
toSorted()toReversed()toSpliced()
They return copies instead of changing the original array, which matters a lot if you work in React and rely on immutability for state.
The problem with in-place array methods
In JavaScript, traditional methods like .sort(), .reverse(), and .splice() mutate the array they’re called on:
const numbers = [3, 1, 2];
numbers.sort(); // Mutates the array 😬
console.log(numbers); // [1, 2, 3]
In frameworks like React, this can lead to unexpected behavior when updating state, because mutating arrays directly doesn’t trigger re-renders.
Comparing old vs. new
| Operation | Mutating method | Non-mutating alternative |
|---|---|---|
| Sort | arr.sort() |
arr.toSorted() |
| Reverse | arr.reverse() |
arr.toReversed() |
| Splice | arr.splice() |
arr.toSpliced() |
These new methods behave similarly to their mutating counterparts, but return a new array instead of modifying the original.
The solution: safe, non-mutating methods
ES2023 introduced non-mutating versions of these familiar array methods:
toSorted()
Creates a sorted copy of the array without changing the original.
const numbers = [3, 1, 2];
const sorted = numbers.toSorted();
console.log(sorted); // [1, 2, 3]
console.log(numbers); // [3, 1, 2] ✅
// ‼️ Contrast: .sort() mutates the array
numbers.sort();
console.log(numbers); // [1, 2, 3] ❌
You can also pass a custom compare function, just like .sort():
const users = [
{ name: 'Kristen', age: 36 },
{ name: 'David', age: 34 },
];
const byAge = users.toSorted((a, b) => a.age - b.age);
console.log(byAge); // [ { name: 'David', age: 34 }, { name: 'Kristen', age: 36 } ]
console.log(users); // [ { name: 'Kristen', age: 36 }, { name: 'David', age: 34 } ] ✅
toReversed()
Returns a reversed copy of the array:
const names = ['Kristen', 'David', 'Ben'];
const reversed = names.toReversed();
console.log(reversed); // ['Ben', 'David', 'Kristen']
console.log(names); // ['Kristen', 'David', 'Ben'] ✅
// ‼️ Contrast: .reverse() mutates the array
names.reverse();
console.log(names); // ['Ben', 'David', 'Kristen'] ❌
Handy when you want to display a list in reverse without touching the source array.
toSpliced()
A safer alternative to .splice(). It returns a new array with elements added/removed, but doesn’t touch the original:
const items = ['a', 'b', 'c', 'd'];
// Remove 1 item at index 1
const withoutB = items.toSpliced(1, 1);
console.log(withoutB); // ['a', 'c', 'd']
console.log(items); // ['a', 'b', 'c', 'd'] ✅
// Add new element at index 2
const withX = items.toSpliced(2, 0, 'x');
console.log(withX); // ['a', 'b', 'x', 'c', 'd']
// ‼️ Contrast: .splice() mutates the array
const items2 = ['a', 'b', 'c', 'd'];
const removed = items2.splice(1, 1);
console.log(removed); // ['b']
console.log(items2); // ['a', 'c', 'd'] ❌
Why this matters in React
In React, immutability is key for triggering component updates and preserving state predictability.
// ❌ Mutating state directly (bad)
state.items.sort(); // No re-render
// ✅ Using toSorted (good)
const sortedItems = state.items.toSorted();
setState({ items: sortedItems }); // Triggers re-render
These new methods help ensure you’re treating arrays as immutable, without needing structuredClone() or deep-copy workarounds.
Real-world example: sorting tasks in React
Here’s how you might use toSorted() or toReversed() in a component to safely display dynamic lists:
function TaskList({ tasks }) {
// Show most recent tasks first
const recentFirst = tasks?.toReversed() ?? [];
return (
<ul>
{recentFirst.map((task) => (
<li key={task.id}>{task.title}</li>
))}
</ul>
);
}
This avoids mutating tasks, which could cause bugs if it’s a prop or derived from state.
The optional chaining (?.) and nullish coalescing (??) operators also help avoid errors if tasks is undefined.
Using them today
These methods don’t need a new mental model. They’re the same methods you already use, minus the mutation. If you’re in a modern environment, or have a build step like Babel or SWC, you can use them today.
Browser support
toSorted(), toReversed(), and toSpliced() are supported in all modern environments (Chrome/Edge 110+, Safari 16+, Firefox 115+, Node.js 20+). For legacy environments, you can use a polyfill like core-js.
At a glance
| Method | What it does | Mutates original? |
|---|---|---|
.toSorted() |
Returns a sorted copy | ❌ |
.toReversed() |
Returns a reversed copy | ❌ |
.toSpliced() |
Returns modified copy (add/remove elements) | ❌ |
These are not flashy next to something like top-level await, but they remove a whole class of mutation bugs. Once you start using them, you’ll never look at .sort() with the same trust again.