Insert, Update, Remove Items by Index
Index-based mutations are where most array bugs come from: `splice` mutates in place and is easy to misuse, while `slice` plus spread gives you a copy without mutating the input. This snippet contrasts the mutable and immutable forms for the four common index operations (remove by index, update by index, insert at index, remove all matching a predicate). Keep mutation explicit at the call site so reviewers know whether the original array survives.
333 views
8
// Mutable: splice changes the original array and returns the removed slice.
const mutableRemoveAt = (arr, i) => {
arr.splice(i, 1);
return arr;
};
// Immutable: slice + spread leaves the input untouched.
const removeAt = (arr, i) => [...arr.slice(0, i), ...arr.slice(i + 1)];
const original = ['a', 'b', 'c', 'd'];
const copy = removeAt(original, 1);
console.log(copy); // ['a', 'c', 'd']
console.log(original); // ['a', 'b', 'c', 'd'] (unchanged)
console.log(mutableRemoveAt(['a', 'b', 'c', 'd'], 1)); // ['a', 'c', 'd']splice(i, 1) removes one item starting at index i and mutates the array, which is fast but surprising in shared state. The immutable form copies the prefix and suffix around the removed index using slice and the spread operator, so callers can rely on the original array surviving. Pick the mutable form only when you own the array and the mutation is the explicit intent (a queue you are draining, for example). For React state, props, or any data passed across module boundaries, default to the immutable variant so equality checks like prev !== next keep working.
// Mutable: assign in place.
const nums = [10, 20, 30, 40];
nums[1] = 99;
console.log(nums); // [10, 99, 30, 40]
// Immutable via slice + spread (works in any modern engine).
const updateAt = (arr, i, value) => [...arr.slice(0, i), value, ...arr.slice(i + 1)];
console.log(updateAt([10, 20, 30, 40], 1, 99)); // [10, 99, 30, 40]
// Immutable via Array.prototype.with (Node 20+, ES2023).
console.log([10, 20, 30, 40].with(1, 99)); // [10, 99, 30, 40]Plain index assignment is the obvious mutable form, fine for a fresh local array but dangerous on data you do not own. The slice/spread variant builds a new array and is supported everywhere. The newest option, Array.prototype.with(i, v), returns a copy with index i replaced and reads more declaratively, but it requires Node 20 or a modern browser. When i is out of range, with throws a RangeError, while the slice form silently appends to the end, so handle that case explicitly if your input range is uncertain.
// Mutable: splice(i, 0, item) inserts without removing.
const inPlaceInsert = (arr, i, item) => {
arr.splice(i, 0, item);
return arr;
};
console.log(inPlaceInsert(['john', 'jane', 'bar'], 2, 'foo'));
// ['john', 'jane', 'foo', 'bar']
// Immutable: spread before, item, spread after.
const insertAt = (arr, i, item) => [...arr.slice(0, i), item, ...arr.slice(i)];
console.log(insertAt(['john', 'jane', 'bar'], 2, 'foo'));
// ['john', 'jane', 'foo', 'bar']
// Insert multiple items at once.
const insertManyAt = (arr, i, items) => [...arr.slice(0, i), ...items, ...arr.slice(i)];
console.log(insertManyAt([1, 2, 5], 2, [3, 4])); // [1, 2, 3, 4, 5]splice(i, 0, item) is the canonical in-place insert: zero items removed, one item inserted at i. The immutable form mirrors the remove/update recipes, with the new item slotted between the prefix and the suffix. For multi-item inserts, spread the new items in place of a single value so you do not pay an extra copy. Note that you can attach extra properties or methods to an array (e.g. arr.label = 'fruits') since arrays are objects, but those properties do not survive slice, concat, or [...arr], so prefer a wrapping object when you need them to persist.
// Immutable, idiomatic: filter keeps everything that should stay.
const removeWhere = (arr, predicate) => arr.filter((item) => !predicate(item));
const items = [
{ id: 1, status: 'active' },
{ id: 2, status: 'archived' },
{ id: 3, status: 'active' },
{ id: 4, status: 'archived' }
];
console.log(removeWhere(items, (it) => it.status === 'archived'));
// [{ id: 1, ... }, { id: 3, ... }]
// Mutable in-place removal: walk backwards so splicing does not skip items.
const removeWhereInPlace = (arr, predicate) => {
for (let i = arr.length - 1; i >= 0; i--) {
if (predicate(arr[i])) arr.splice(i, 1);
}
return arr;
};
console.log(removeWhereInPlace([1, 2, 3, 4, 5, 6], (n) => n % 2 === 0));
// [1, 3, 5]filter is the right default: declarative, immutable, and obvious to read. Note that you pass the predicate that decides which items to KEEP, so negate it when the natural phrasing is "remove these". When you must mutate in place (memory-tight loops, very large arrays), iterate backwards so removing index i does not change the index of the next item to inspect. A forward loop with splice skips elements right after a removal and produces silently wrong output. Both forms are linear time; the immutable version trades a copy for safety, which is almost always the right call.
