Updating State in React: Best Practices & Examples
Updating State the Right Way
The Two Rules
State updates cause two classes of bug for beginners. Both have simple rules.
Rule 1: State updates are not instant
jsx
function handleClick() {
setCount(count + 1);
console.log(count); // Still shows the OLD value
}React schedules the update and applies it before the next render. Reading the variable immediately after calling the setter gives you the old value.
This matters when you update the same state twice:
jsx
// Wrong — both lines use the same old count, so it only increases by 1
setCount(count + 1);
setCount(count + 1);
// Correct — the functional form uses the latest value each time
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);The functional form passing a function instead of a value is the safe choice whenever the new state depends on the old state.
Rule 2: Never modify objects or arrays directly
React compares the old and new state to decide whether to re-render. If you change an existing object, the old and new references are the same, so React sees no change.
jsx
const [student, setStudent] = useState({ name: "Priya", progress: 40 });
// Wrong — React does not detect this
student.progress = 60;
setStudent(student);
// Correct — create a new object
setStudent({ ...student, progress: 60 });The ...student spread copies all existing properties, and progress: 60 overwrites one.
Arrays Follow the Same Rule
jsx
const [tasks, setTasks] = useState(["Watch lesson 1", "Solve exercise 1"]);
// Adding — create a new array
setTasks([...tasks, "Watch lesson 2"]);
// Removing — filter returns a new array
setTasks(tasks.filter(task => task !== "Solve exercise 1"));
// Updating one item — map returns a new array
setTasks(tasks.map(task => task === "Watch lesson 1" ? "Watch lesson 1 (done)" : task));Avoid push(), pop(), and splice() on state arrays — they modify the original. Use spread, filter(), and map(), which return new arrays.
Modifies the original (avoid) | Returns a new array (use) |
| push(), unshift() | [...array, item] |
| pop(), splice() | filter() |
| direct index assignment | map() |
| sort(), reverse() | [...array].sort() |
Example
src/components/TaskList.jsx
jsx
import { useState } from "react";
function TaskList() {
const [tasks, setTasks] = useState([
{ id: 1, text: "Complete JSX lesson", done: true },
{ id: 2, text: "Build a course card", done: false }
]);
function toggleTask(id) {
setTasks(tasks.map(task =>
task.id === id ? { ...task, done: !task.done } : task
));
}
return (
<ul>
{tasks.map(task => (
<li key={task.id}>
<span>{task.done ? "✓" : "○"} {task.text}</span>
<button onClick={() => toggleTask(task.id)}>Toggle</button>
</li>
))}
</ul>
);
}
export default TaskList;How It Works
- tasks.map(...) builds a brand-new array.
- For the matching task, { ...task, done: !task.done } creates a new object with the flag flipped.
- Every other task is returned unchanged.
- React sees a new array reference and re-renders.








