"I was interviewing a candidate for a senior frontend role last week."
I asked them to update a user's address inside an array of profiles without mutating the original state. They immediately reached for splice, mutating the state array directly and causing React to ignore the update. That is when I realized that developers still struggle with mutating vs non-mutating array methods.
Arrays are the most versatile data structure in JavaScript. When working with them, you constantly perform **CRUD** operations: Creating new elements, Reading items, Updating values, and Deleting records.
However, many built-in JavaScript array methods mutate (modify) the original array directly. In modern, component-driven frameworks like React, mutating state directly breaks reference checking, causing components to skip re-rendering.
This guide details both mutating and non-mutating array methods, showing you how to perform clean CRUD operations in vanilla JavaScript and modern frameworks.
02. Create: Inserting Elements (push, unshift)
Adding new elements to an array depends on whether you want to insert them at the beginning or the end:
push()(Mutating): Adds one or more elements to the end of the array and returns the new array length.unshift()(Mutating): Adds one or more elements to the beginning of the array and returns the new array length.
const fruits = ["Banana", "Cherry"];
// 1. Add to the end (Create / push)
fruits.push("Date");
console.log(fruits); // ["Banana", "Cherry", "Date"]
// 2. Add to the beginning (Create / unshift)
fruits.unshift("Apple");
console.log(fruits); // ["Apple", "Banana", "Cherry", "Date"]
03. Read: Copying and Selecting (slice)
Reading elements can be done by index, or you can extract a segment of an array using the non-mutating slice() method:
slice(start, end) (Non-Mutating): Returns a shallow copy of a portion of an array. The original array remains completely unchanged.
const plants = ["Fern", "Ivy", "Oak", "Pine"];
// Extract indices 1 and 2 (end index is non-inclusive)
const partialPlants = plants.slice(1, 3);
console.log(partialPlants); // ["Ivy", "Oak"]
console.log(plants); // ["Fern", "Ivy", "Oak", "Pine"] (Unchanged!)
04. Update: Modifying Elements (splice)
Updating array items at specific indices can be achieved using direct assignment or the mutating splice() method.
splice(start, deleteCount, item1, item2, ...) (Mutating): Modifies the contents of an array by removing or replacing existing elements and/or adding new elements in place.
const tools = ["Hammer", "Screwdriver", "Wrench"];
// Replace "Screwdriver" (index 1) with "Drill"
// splice returns the removed elements
const removed = tools.splice(1, 1, "Drill");
console.log(tools); // ["Hammer", "Drill", "Wrench"]
console.log(removed); // ["Screwdriver"]
05. Delete: Removing Elements (pop, shift, splice)
Removing elements from an array varies by position:
pop()(Mutating): Removes the last element of the array and returns that element.shift()(Mutating): Removes the first element of the array and returns that element.splice()(Mutating): Removes elements from any index you specify.
const animals = ["Lion", "Tiger", "Bear", "Wolf"];
// 1. Remove from the end (pop)
animals.pop(); // Returns "Wolf"
console.log(animals); // ["Lion", "Tiger", "Bear"]
// 2. Remove from the beginning (shift)
animals.shift(); // Returns "Lion"
console.log(animals); // ["Tiger", "Bear"]
// 3. Remove from middle index 1
animals.splice(1, 1); // Removes "Bear"
console.log(animals); // ["Tiger"]
06. Immutable CRUD: Best Practices for Component State
In frameworks like React or Vue, mutating state directly prevents components from re-rendering because their reference pointers remain unchanged.
To update state safely, use the **spread operator** ([...]) to clone the array before performing updates:
const originalCart = ["Shirt", "Shoes"];
// Secure, non-mutating update (Create)
const updatedCart = [...originalCart, "Hat"];
console.log(originalCart); // ["Shirt", "Shoes"] (Safe)
console.log(updatedCart); // ["Shirt", "Shoes", "Hat"]
07. Interactive Homework Challenges
To practice these concepts, complete the programming exercises below. Copy the code into your browser's console or a Node.js runtime and write the missing logic:
Challenge 1: Immutable Insert
Write a function insertAt(arr, index, element) that returns a new array with the element inserted at the specified index without mutating the original array.
function insertAt(arr, index, element) {
// TODO: Implement using slice and spread operator
}
const baseline = ["Red", "Blue", "Green"];
const updated = insertAt(baseline, 1, "Yellow");
console.log(updated); // Should output: ["Red", "Yellow", "Blue", "Green"]
console.log(baseline); // Should output: ["Red", "Blue", "Green"] (Verify no mutation)
Challenge 2: Safe Object Search & Update
Write a function updateUserStatus(users, targetId, newStatus) that updates a user's status within an array of user objects without mutating the original array.
function updateUserStatus(users, targetId, newStatus) {
// TODO: Use map to return a new array with the target user's status updated
}
const users = [
{ id: 1, name: "Alice", status: "pending" },
{ id: 2, name: "Bob", status: "pending" }
];
const updatedUsers = updateUserStatus(users, 2, "active");
console.log(updatedUsers[1].status); // Should output: "active"
console.log(users[1].status); // Should output: "pending" (Verify no mutation)
To test your skills in managing JavaScript scope and variables, explore our [INTERNAL LINK: frontend coding challenges], or join our [INTERNAL LINK: React Masterclass learning path]. You can also book [INTERNAL LINK: 1:1 expert mentorship sessions] with our senior engineers to audit your application structure.