Mutating and non-mutating Array methods in JavaScript
Mutating array methods change the original array in place. Non-mutating methods return new data without touching the original.
Theory
TL;DR
- Mutating:
push(),pop(),splice(),sort(),reverse()- the array changes after the call - Non-mutating:
map(),filter(),slice(),concat(),find()- original stays the same - Analogy: mutating is editing a document directly; non-mutating is making a copy first and editing that
- React and Redux require non-mutating methods - mutation bypasses change detection entirely
- Quick fix for sort:
[...arr].sort()gives you a sorted copy without changingarr
Quick example
const original = [3, 1, 2];
// sort() mutates - original changes
original.sort();
console.log(original); // [1, 2, 3] - changed
// spread + sort - original stays safe
const nums = [3, 1, 2];
const sorted = [...nums].sort();
console.log(nums); // [3, 1, 2] - unchanged
console.log(sorted); // [1, 2, 3] - new arraysort() returns a reference to the same array it modified, not a new one. That catches a lot of developers who expect it to behave like map().
Key difference
Mutating methods modify the array's memory directly. Non-mutating methods allocate new memory, compute values, and return the result. If two variables point to the same array, one mutation affects both. React's change detection compares references (arr !== prevArr), so mutating an array makes React think nothing changed and skip the re-render.
When to use
- Mutating methods: when you own the array and no other code holds a reference to it; in tight loops where allocating new arrays is measurably slow
- Non-mutating methods: default choice in React state updates, Redux reducers, and any function that receives an array as a parameter
- Mixed approach: mutate inside a function that owns the data, then return the result as a new value to callers
Comparison table
| Method | Type | Returns | Modifies original | Use case |
|---|---|---|---|---|
push() | Mutating | New length | Yes | Adding to an array you own |
concat() | Non-mutating | New array | No | Combining arrays safely |
splice() | Mutating | Removed elements | Yes | Removing/inserting at index |
slice() | Non-mutating | New array | No | Getting a portion safely |
sort() | Mutating | Same array (sorted) | Yes | Sorting when mutation is acceptable |
toSorted() | Non-mutating | New array | No | Sorting without side effects (ES2023) |
reverse() | Mutating | Same array (reversed) | Yes | Reversing when mutation is acceptable |
toReversed() | Non-mutating | New array | No | Reversing without side effects (ES2023) |
map() | Non-mutating | New array | No | Transforming data (React, Redux) |
filter() | Non-mutating | New array | No | Filtering data (React, Redux) |
find() | Non-mutating | Single element | No | Searching without changes |
fill() | Mutating | Same array | Yes | Initializing array values |
Common mistakes
Mistake 1: Mutating state in React
// Wrong - mutation, React skips the re-render
const handleAdd = () => {
state.items.push(newItem);
setState(state); // same reference - React sees no change
};
// Right - new array, React detects the change
const handleAdd = () => {
setState([...state.items, newItem]);
};React compares prevState === newState. Mutation keeps the same reference, so the check passes and the component stays stale. Always create a new array in state updates.
Mistake 2: Expecting splice() to return the modified array
const arr = [1, 2, 3];
const result = arr.splice(1, 1);
console.log(result); // [2] - the REMOVED element, not [1, 3]
console.log(arr); // [1, 3] - the modified array, not in resultThe splice() return value catches almost everyone the first time. You expect the resulting array, you get the removed elements instead. Use arr directly after calling splice, or switch to filter() for non-mutating behavior.
Mistake 3: Sorting numbers without a comparator
const numbers = [10, 5, 40, 25];
numbers.sort();
console.log(numbers); // [10, 25, 40, 5] - wrong, sorted as strings
numbers.sort((a, b) => a - b);
console.log(numbers); // [5, 10, 25, 40] - correctWithout a comparator, sort() converts elements to strings and compares character codes. "40" comes before "5" alphabetically. For numbers, always pass (a, b) => a - b.
Mistake 4: Mutating an array while iterating it
// Wrong - skips elements unpredictably
const arr = [1, 2, 3, 4];
arr.forEach(item => {
if (item === 2) arr.splice(arr.indexOf(item), 1);
});
// Right - filter creates a new array
const filtered = [1, 2, 3, 4].filter(item => item !== 2);
console.log(filtered); // [1, 3, 4]Splicing during iteration shifts indices and causes elements to be skipped. Use filter() instead.
Real-world usage
- React:
map(),filter(),concat(), spread operator for all state updates - Redux: reducers must return new state -
filter(),map(), spread syntax; neverpush()orsplice() - Vue.js: non-mutating methods keep the reactive system working correctly
- Node.js/Express: mutating methods are fine inside route handlers that own and build the response data
- ES2023:
toSorted(),toReversed(),toSpliced()are the built-in non-mutating alternatives - no need to spread before sorting
Follow-up questions
Q: Why does React require non-mutating array methods?
A: React uses reference equality (prevState === newState) to detect changes. Mutating an array keeps the same reference, so the check returns true and React skips the re-render. A new array always has a different reference.
Q: What is the performance cost of non-mutating methods?
A: Non-mutating methods allocate new memory and copy data. For arrays under 1000 elements the difference is negligible. In tight loops with large arrays, mutating can matter. For most application code, immutability is worth the small overhead.
Q: How do I make sort() non-mutating without ES2023?
A: Create a copy first: [...arr].sort() or arr.slice().sort(). Both produce a shallow copy, so sorting the copy leaves the original untouched.
Q: (Senior) Why does map() always return a new array, but sort() modifies the original?
A: sort() rearranges elements in place and reuses the existing memory allocation - a performance-conscious decision from the original spec. map() produces transformed values and there is no way to do that without new memory, because the values themselves change. The split comes from algorithmic necessity as much as design.
Examples
React state update with sort
function TodoList({ todos, setTodos }) {
// Wrong - mutates todos, React won't re-render
const handleSortWrong = () => {
todos.sort((a, b) => a.priority - b.priority);
setTodos(todos); // same reference, no re-render
};
// Right - new array, React detects the change
const handleSort = () => {
const sorted = [...todos].sort((a, b) => a.priority - b.priority);
setTodos(sorted);
};
return <ul>{todos.map(t => <li key={t.id}>{t.text}</li>)}</ul>;
}[...todos] creates a shallow copy of the array. Sorting that copy produces a new reference, which React sees as a change and re-renders the component.
splice() vs slice() - same name, opposite behavior
const arr = [1, 2, 3, 4, 5];
// splice() - mutates, returns removed elements
const removed = arr.splice(1, 2);
console.log(removed); // [2, 3] - what was removed
console.log(arr); // [1, 4, 5] - what remains (arr is changed)
// slice() - no mutation, returns what you keep
const arr2 = [1, 2, 3, 4, 5];
const kept = arr2.slice(1, 3);
console.log(kept); // [2, 3] - what you kept
console.log(arr2); // [1, 2, 3, 4, 5] - unchangedsplice() and slice() look similar but work oppositely. splice() mutates and returns what was removed. slice() returns what you selected and leaves the original alone.
Numeric sort gotcha
const prices = [100, 25, 1000, 50];
// Default sort treats numbers as strings
prices.sort();
console.log(prices); // [100, 1000, 25, 50] - alphabetical, not numeric
// Non-mutating sort with a numeric comparator
const sortedPrices = [...prices].sort((a, b) => a - b);
console.log(sortedPrices); // [25, 50, 100, 1000] - correct
console.log(prices); // [100, 1000, 25, 50] - original unchangedDefault sort() uses string comparison. "1000" comes before "25" alphabetically. For numeric sorting, always provide a comparator. The spread + sort pattern keeps the original intact.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.