Problem Solved

React · update array in state

Home › Courses › React › Problems › Update array in state
⚛️ Problem · 6 min read

How to update an array in state.
Add, remove, edit, sort — no mutation.

Arrays are the most common collection in React state — to-do lists, chat messages, search results. Every update must produce a new array, not mutate the old one. The methods you already know (push, splice, sort) are exactly the ones you can't use. This page covers every array operation with the immutable pattern.

Short answer: Use methods that return new arrays: filter (remove), map (update), spread (add), slice / spread (copy). Never use push, splice, sort, reverse — they mutate. For sorting, copy first: [...items].sort(). Always use the functional setter: setItems(prev => ...). When in doubt: setItems(prev => produceNewArray(prev)).

✅ Solution 1 — Add, remove, update

The three operations you'll use most.

Each operation has one right pattern. Adding uses spread. Removing uses filter. Updating uses map. All three return a new array — React sees the new reference, re-renders. The old array stays untouched, which is exactly what React needs.

add-remove-update.tsx
import { useState } from 'react';

interface Todo {
id: string;
text: string;
done: boolean;
}

function TodoList() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');

// ─── ADD: spread at the end ──────────────────
function addTodo() {
if (!input.trim()) return;
setTodos(prev => [
...prev,
{ id: crypto.randomUUID(), text: input, done: false }
]);
setInput('');
}

// ─── ADD at the beginning ────────────────────
function addToTop() {
setTodos(prev => [
{ id: crypto.randomUUID(), text: input, done: false },
...prev
]);
}

// ─── REMOVE: filter ──────────────────────────
function removeTodo(id: string) {
setTodos(prev => prev.filter(t => t.id !== id));
}

// ─── UPDATE: map ─────────────────────────────
function toggleTodo(id: string) {
setTodos(prev =>
prev.map(t => t.id === id ? { ...t, done: !t.done } : t)
);
}

// ─── UPDATE multiple fields ──────────────────
function updateText(id: string, text: string) {
setTodos(prev =>
prev.map(t => t.id === id ? { ...t, text } : t)
);
}

// ─── CLEAR ALL ───────────────────────────────
function clearAll() {
setTodos([]);
}

// ─── REPLACE entire array ────────────────────
function loadFromServer() {
fetch('/api/todos')
.then(r => r.json())
.then(setTodos);
}

return (
setInput(e.target.value)} />
    {todos.map(todo => (
  • "checkbox" checked={todo.done} onChange={() => toggleTodo(todo.id)} /> 'line-through' : 'none' }}> {todo.text}
  • ))}
); } // ─── What NOT to do ────────────────────────── // // ❌ todos.push(newTodo); — mutates // ❌ setTodos(todos); — same reference // // ❌ todos.splice(index, 1); — mutates // ❌ setTodos(todos); — same reference // // ❌ todos[index] = newValue; — mutates // ❌ setTodos(todos); — same reference // // React compares state by reference. Mutation // keeps the same reference, so no re-render. // // ───────────────────────────────────────────── // // ✅ Add: setTodos([...prev, item]) // ✅ Add at top: setTodos([item, ...prev]) // ✅ Remove: setTodos(prev.filter(...)) // ✅ Update: setTodos(prev.map(...)) // ✅ Clear: setTodos([]) // ✅ Replace: setTodos(newArray) console.log(TodoList);
import { useState } from 'react';

function TodoList() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');

function addTodo() {
if (!input.trim()) return;
setTodos(prev => [...prev, { id: crypto.randomUUID(), text: input, done: false }]);
setInput('');
}

function addToTop() {
setTodos(prev => [{ id: crypto.randomUUID(), text: input, done: false }, ...prev]);
}

function removeTodo(id) {
setTodos(prev => prev.filter(t => t.id !== id));
}

function toggleTodo(id) {
setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t));
}

function updateText(id, text) {
setTodos(prev => prev.map(t => t.id === id ? { ...t, text } : t));
}

function clearAll() {
setTodos([]);
}

function loadFromServer() {
fetch('/api/todos').then(r => r.json()).then(setTodos);
}

return React.createElement('div', null,
React.createElement('div', null,
React.createElement('input', { value: input, onChange: e => setInput(e.target.value) }),
React.createElement('button', { onClick: addTodo }, 'Add')
),
React.createElement('ul', null,
todos.map(todo =>
React.createElement('li', { key: todo.id },
React.createElement('input', {
type: 'checkbox',
checked: todo.done,
onChange: () => toggleTodo(todo.id)
}),
React.createElement('span', {
style: { textDecoration: todo.done ? 'line-through' : 'none' }
}, todo.text),
React.createElement('button', { onClick: () => removeTodo(todo.id) }, '×')
)
)
),
React.createElement('button', { onClick: clearAll }, 'Clear all')
);
}

console.log(TodoList);

Add = spread

[...prev, item] — new array, item at end. [item, ...prev] for beginning.

Remove = filter

prev.filter(i => i.id !== id) — new array without matching items. Never splice.

Update = map

prev.map(i => i.id === id ? {...i, done: !i.done} : i) — new array, item updated, others preserved.

Never mutate

push, splice, sort, reverse — all mutate. React sees the same reference, no re-render.

Memorize four methods: filter, map, spread, slice. They cover every array update. filter removes. map transforms. Spread adds. slice copies. Every array operation you'll ever need is built on these four. Push, splice, sort — the mutating methods — you'll never call on state. Once you internalize the four immutable methods, array updates become mechanical.

✅ Solution 2 — Sort, reverse, and other mutating methods

Copy first, then mutate the copy.

Some array methods only come in mutating form — sort, reverse, splice. You still need them. The fix: copy the array with spread, then call the method on the copy. The original stays untouched; the sorted/reversed result is a new array.

sort-reverse.tsx
import { useState } from 'react';

interface Item {
id: string;
name: string;
price: number;
}

function SortableList() {
const [items, setItems] = useState([
{ id: '1', name: 'Charlie', price: 30 },
{ id: '2', name: 'Alice', price: 10 },
{ id: '3', name: 'Bob', price: 20 }
]);

// ─── BAD: sort mutates state ─────────────────
function badSort() {
items.sort((a, b) => a.name.localeCompare(b.name));
setItems(items); // same reference — no re-render
}

// ─── GOOD: copy, then sort ───────────────────
function sortByName() {
setItems(prev =>
[...prev].sort((a, b) => a.name.localeCompare(b.name))
);
}

function sortByPrice() {
setItems(prev =>
[...prev].sort((a, b) => a.price - b.price)
);
}

// ─── Reverse: same pattern ───────────────────
function reverseList() {
setItems(prev => [...prev].reverse());
}

// ─── Splice: insert at index (immutably) ─────
function insertAt(index: number, item: Item) {
setItems(prev => [
...prev.slice(0, index),
item,
...prev.slice(index)
]);
}

// ─── Splice: replace at index ────────────────
function replaceAt(index: number, item: Item) {
setItems(prev => [
...prev.slice(0, index),
item,
...prev.slice(index + 1)
]);
}

// ─── Splice: remove at index ─────────────────
function removeAt(index: number) {
setItems(prev => [
...prev.slice(0, index),
...prev.slice(index + 1)
]);
}

// ─── More idiomatic: use filter/map ─────────
// For "remove at index", the filter version is cleaner:
// setItems(prev => prev.filter((_, i) => i !== index))
// For "replace at index", the map version is cleaner:
// setItems(prev => prev.map((item, i) => i === index ? newItem : item))
// For "insert at index", slice + spread is the pattern.

// ─── Advanced: sorted derived value ──────────
// Instead of sorting state, keep it unsorted and sort on render:
const sortedItems = [...items].sort((a, b) => a.name.localeCompare(b.name));

return (
    {sortedItems.map(item => (
  • {item.name}: $`{item.price}
  • ))}
); } // ─── The mutating methods and their immutable fixes // // sort(): [...prev].sort(...) or prev.toSorted(...) // reverse(): [...prev].reverse() or prev.toReversed() // splice(): [...] slice spread pattern or prev.toSpliced(...) // push(): [...prev, item] // unshift(): [item, ...prev] // pop(): prev.slice(0, -1) // shift(): prev.slice(1) // // ES2023 added toSorted, toReversed, toSpliced — they // return new arrays instead of mutating. Use them when // available; fall back to [...prev].method() for older // targets. console.log(SortableList);
import { useState } from 'react';

function SortableList() {
const [items, setItems] = useState([
{ id: '1', name: 'Charlie', price: 30 },
{ id: '2', name: 'Alice', price: 10 },
{ id: '3', name: 'Bob', price: 20 }
]);

function badSort() {
items.sort((a, b) => a.name.localeCompare(b.name));
setItems(items);
}

function sortByName() {
setItems(prev => [...prev].sort((a, b) => a.name.localeCompare(b.name)));
}

function sortByPrice() {
setItems(prev => [...prev].sort((a, b) => a.price - b.price));
}

function reverseList() {
setItems(prev => [...prev].reverse());
}

function insertAt(index, item) {
setItems(prev => [
...prev.slice(0, index),
item,
...prev.slice(index)
]);
}

function replaceAt(index, item) {
setItems(prev => [
...prev.slice(0, index),
item,
...prev.slice(index + 1)
]);
}

function removeAt(index) {
setItems(prev => [
...prev.slice(0, index),
...prev.slice(index + 1)
]);
}

const sortedItems = [...items].sort((a, b) => a.name.localeCompare(b.name));

return React.createElement('div', null,
React.createElement('button', { onClick: sortByName }, 'Sort by name'),
React.createElement('button', { onClick: sortByPrice }, 'Sort by price'),
React.createElement('button', { onClick: reverseList }, 'Reverse'),
React.createElement('ul', null,
sortedItems.map(item => React.createElement('li', { key: item.id }, item.name, ': `$', item.price))
)
);
}

console.log(SortableList);

Copy before sort

[...prev].sort() — new array, sorted. Without spread, sort mutates state.

ES2023 immutable methods

toSorted, toReversed, toSpliced — return new arrays. Use them when targeting modern JS.

Slice for insert/remove

[...prev.slice(0, i), item, ...prev.slice(i)] — insert. Without the middle item — remove.

Sort on render instead

If the sort is derived, keep state unsorted. const sorted = [...items].sort(...) during render.

sort mutates in place — this is the #1 array bug. Even experience React developers hit it. items.sort() reorders the original array, then setItems(items) passes the same reference — React sees no change, no re-render. The items appear unsorted even though sort ran. Fix: setItems([...items].sort()) — one spread fixes it. Same for reverse, splice, push, pop, shift, unshift. If the method's name doesn't start with "to" or "slice" or "map" or "filter", it probably mutates.

✅ Solution 3 — Advanced patterns

Nested objects, multiple updates, and reducers.

Real array updates often involve nested objects — updating items[3].tags, or removing items[3].metadata.author. The spread pattern extends to every level. For complex sequences of updates, useReducer is cleaner than multiple setters.

advanced-arrays.tsx
import { useState, useReducer } from 'react';

// ─── Nested object updates ───────────────────
interface Item {
id: string;
name: string;
metadata: { tags: string[]; author: string };
}

function NestedUpdates() {
const [items, setItems] = useState([
{ id: '1', name: 'Item 1', metadata: { tags: ['a'], author: 'Alice' } }
]);

function addTag(id: string, tag: string) {
setItems(prev =>
prev.map(item =>
item.id === id
? {
...item,
metadata: {
...item.metadata,
tags: [...item.metadata.tags, tag]
}
}
: item
)
);
}

function removeTag(id: string, tag: string) {
setItems(prev =>
prev.map(item =>
item.id === id
? {
...item,
metadata: {
...item.metadata,
tags: item.metadata.tags.filter(t => t !== tag)
}
}
: item
)
);
}

return (
    {items.map(item => (
  • {item.name} by {item.metadata.author} {item.metadata.tags.map(tag => ( {tag} ))}
  • ))}
); } // ─── Multiple updates: useReducer ───────────── interface State { items: Item[]; loading: boolean; error: string | null } type Action = | { type: 'load' } | { type: 'success'; items: Item[] } | { type: 'error'; message: string } | { type: 'add'; item: Item } | { type: 'remove'; id: string } | { type: 'update'; id: string; patch: Partial } | { type: 'clear' }; function reducer(state: State, action: Action): State { switch (action.type) { case 'load': return { ...state, loading: true, error: null }; case 'success': return { ...state, loading: false, items: action.items }; case 'error': return { ...state, loading: false, error: action.message }; case 'add': return { ...state, items: [...state.items, action.item] }; case 'remove': return { ...state, items: state.items.filter(i => i.id !== action.id) }; case 'update': return { ...state, items: state.items.map(i => i.id === action.id ? { ...i, ...action.patch } : i) }; case 'clear': return { ...state, items: [] }; } } function ManagedList() { const [state, dispatch] = useReducer(reducer, { items: [], loading: false, error: null }); function loadItems() { dispatch({ type: 'load' }); fetch('/api/items') .then(r => r.json()) .then(items => dispatch({ type: 'success', items })) .catch(e => dispatch({ type: 'error', message: e.message })); } if (state.loading) return

Loading...

; if (state.error) return

Error: {state.error}

; return (
    {state.items.map(i =>
  • {i.name}
  • )}
); } // ─── Derived arrays (don't store) ──────────── function Derived() { const [items, setItems] = useState<number[]>([3, 1, 2]); // ❌ Don't store sorted as state // ✅ Compute during render const sorted = [...items].sort((a, b) => a - b); const total = items.reduce((s, n) => s + n, 0); const evens = items.filter(n => n % 2 === 0); return (

Items: {items.join(', ')}

Sorted: {sorted.join(', ')}

Total: {total}

Evens: {evens.join(', ')}

); } // ─── Summary ───────────────────────────────── // // Add: setItems(prev => [...prev, item]) // Remove: setItems(prev => prev.filter(i => i.id !== id)) // Update: setItems(prev => prev.map(i => i.id === id ? {...i, ...} : i)) // Sort: setItems(prev => [...prev].sort(...)) // Reverse: setItems(prev => [...prev].reverse()) // Insert: setItems(prev => [...prev.slice(0, i), item, ...prev.slice(i)]) // Remove at: setItems(prev => prev.filter((_, idx) => idx !== i)) // Replace at: setItems(prev => prev.map((item, idx) => idx === i ? newItem : item)) // Clear: setItems([]) // // Never: push, pop, shift, unshift, splice, sort, reverse // on state directly. console.log(NestedUpdates, reducer, ManagedList, Derived);
import { useState, useReducer } from 'react';

function NestedUpdates() {
const [items, setItems] = useState([
{ id: '1', name: 'Item 1', metadata: { tags: ['a'], author: 'Alice' } }
]);
function addTag(id, tag) {
setItems(prev =>
prev.map(item =>
item.id === id
? { ...item, metadata: { ...item.metadata, tags: [...item.metadata.tags, tag] } }
: item
)
);
}
function removeTag(id, tag) {
setItems(prev =>
prev.map(item =>
item.id === id
? { ...item, metadata: { ...item.metadata, tags: item.metadata.tags.filter(t => t !== tag) } }
: item
)
);
}
return React.createElement('ul', null,
items.map(item =>
React.createElement('li', { key: item.id },
item.name, ' by ', item.metadata.author,
item.metadata.tags.map(tag =>
React.createElement('span', { key: tag },
tag,
React.createElement('button', { onClick: () => removeTag(item.id, tag) }, '×')
)
)
)
)
);
}

function reducer(state, action) {
switch (action.type) {
case 'load': return { ...state, loading: true, error: null };
case 'success': return { ...state, loading: false, items: action.items };
case 'error': return { ...state, loading: false, error: action.message };
case 'add': return { ...state, items: [...state.items, action.item] };
case 'remove': return { ...state, items: state.items.filter(i => i.id !== action.id) };
case 'update': return { ...state, items: state.items.map(i => i.id === action.id ? { ...i, ...action.patch } : i) };
case 'clear': return { ...state, items: [] };
}
}

function ManagedList() {
const [state, dispatch] = useReducer(reducer, { items: [], loading: false, error: null });
function loadItems() {
dispatch({ type: 'load' });
fetch('/api/items')
.then(r => r.json())
.then(items => dispatch({ type: 'success', items }))
.catch(e => dispatch({ type: 'error', message: e.message }));
}
if (state.loading) return React.createElement('p', null, 'Loading...');
if (state.error) return React.createElement('p', null, 'Error: ', state.error);
return React.createElement('ul', null,
state.items.map(i => React.createElement('li', { key: i.id }, i.name))
);
}

function Derived() {
const [items, setItems] = useState([3, 1, 2]);
const sorted = [...items].sort((a, b) => a - b);
const total = items.reduce((s, n) => s + n, 0);
const evens = items.filter(n => n % 2 === 0);
return React.createElement('div', null,
React.createElement('p', null, 'Items: ', items.join(', ')),
React.createElement('p', null, 'Sorted: ', sorted.join(', ')),
React.createElement('p', null, 'Total: ', total),
React.createElement('p', null, 'Evens: ', evens.join(', '))
);
}

console.log(NestedUpdates, reducer, ManagedList, Derived);

Nested: spread every level

{'{...item, metadata: {...item.metadata, tags: [...]}}'} — copy the item, copy its metadata, update the tags.

useReducer for many updates

Add, remove, update, clear — each is one action. One reducer handles them all with consistent patterns.

Derive, don't store

Sorted, filtered, total — compute during render. Storing them causes sync bugs.

The complete toolkit

filter, map, spread, slice for updates. useReducer for multiple related operations. useMemo for expensive derivations.

⚠️ Common mistakes

The array update bugs that cost real time.

1. Using push on state

items.push(x); setItems(items) — same reference. Fix: setItems([...items, x]).

2. Using splice on state

items.splice(i, 1) — mutates. Fix: items.filter((_, idx) => idx !== i).

3. Using sort on state directly

items.sort() — mutates. Fix: [...items].sort() or items.toSorted().

4. Mutating array elements

items[0].done = true — mutates. Fix: items.map(i => i.id === id ? {...i, done: true} : i).

5. Forgetting functional setter

setItems([...items, x]) uses stale items if called twice fast. Fix: setItems(prev => [...prev, x]).

6. Assigning new array to same variable

items = [...items, x]; setItems(items) — works but confusing. Fix: setItems(prev => [...prev, x]).

7. Using index as key in a mutable list

After sort/reorder, index keys cause state leaks. Fix: key={item.id} — stable identity.

8. Storing derived arrays

sortedItems in state, synced with useEffect. Fix: compute during render.

❓ Frequently asked

How do I add an item to an array in React state?

Use the spread operator: setItems([...items, newItem]) or, with the functional form, setItems(prev => [...prev, newItem]). This creates a new array with the new item appended. Never use items.push(newItem).

How do I remove an item from an array in state?

Use filter: setItems(prev => prev.filter(i => i.id !== id)). Filter returns a new array without the matching items. Never use splice — it mutates.

How do I update an item in an array in state?

Use map: setItems(prev => prev.map(i => i.id === id ? { ...i, name: 'new' } : i)). Map iterates, returns a new object for the matching item, leaves others unchanged.

How do I sort an array in state?

Sort mutates the original array, so never call it on state directly. Copy first: setItems(prev => [...prev].sort((a, b) => a.name.localeCompare(b.name))). Same for reverse.

🔗 Related

← All React problems
Problem solved · React