Skip to main content

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 changing arr

Quick example

js
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 array

sort() 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

MethodTypeReturnsModifies originalUse case
push()MutatingNew lengthYesAdding to an array you own
concat()Non-mutatingNew arrayNoCombining arrays safely
splice()MutatingRemoved elementsYesRemoving/inserting at index
slice()Non-mutatingNew arrayNoGetting a portion safely
sort()MutatingSame array (sorted)YesSorting when mutation is acceptable
toSorted()Non-mutatingNew arrayNoSorting without side effects (ES2023)
reverse()MutatingSame array (reversed)YesReversing when mutation is acceptable
toReversed()Non-mutatingNew arrayNoReversing without side effects (ES2023)
map()Non-mutatingNew arrayNoTransforming data (React, Redux)
filter()Non-mutatingNew arrayNoFiltering data (React, Redux)
find()Non-mutatingSingle elementNoSearching without changes
fill()MutatingSame arrayYesInitializing array values

Common mistakes

Mistake 1: Mutating state in React

js
// 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

js
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 result

The 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

js
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] - correct

Without 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

js
// 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; never push() or splice()
  • 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

js
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

js
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] - unchanged

splice() 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

js
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 unchanged

Default 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?