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.
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 ( ); } // ─── 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);setInput(e.target.value)} />{todos.map(todo => (
- "checkbox" checked={todo.done} onChange={() => toggleTodo(todo.id)} /> 'line-through' : 'none' }}> {todo.text}
))}
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.
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 (
); } // ─── 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);{sortedItems.map(item => (
- {item.name}: $`{item.price}
))}
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.
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 => (
); } // ─── 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- {item.name} by {item.metadata.author} {item.metadata.tags.map(tag => ( {tag} ))}
))}- } | { 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) returnError: {state.error}
; return ({state.items.map(i =>
); } // ─── 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 (- {i.name}
)}); } // ─── 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);Items: {items.join(', ')}
Sorted: {sorted.join(', ')}
Total: {total}
Evens: {evens.join(', ')}
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.
push on stateitems.push(x); setItems(items) — same reference. Fix: setItems([...items, x]).
splice on stateitems.splice(i, 1) — mutates. Fix: items.filter((_, idx) => idx !== i).
sort on state directlyitems.sort() — mutates. Fix: [...items].sort() or items.toSorted().
items[0].done = true — mutates. Fix: items.map(i => i.id === id ? {...i, done: true} : i).
setItems([...items, x]) uses stale items if called twice fast. Fix: setItems(prev => [...prev, x]).
items = [...items, x]; setItems(items) — works but confusing. Fix: setItems(prev => [...prev, x]).
After sort/reorder, index keys cause state leaks. Fix: key={item.id} — stable identity.
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.