State & useState
State is the memory of a React component. When a component needs to keep track of information between renders—like whether a modal is open, what a user typed in an input, or data fetched from a server—it uses state.
The useState hook is the fundamental way to add state to functional components.
The useState Hook
To use state, import the useState hook from React. It returns an array with exactly two items:
- The current state variable.
- A function to update that state variable.
import { useState } from 'react';
function Counter() {
// [currentState, updaterFunction] = useState(initialState)
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
State Updates and Re-rendering
When you call the state updater function (e.g., setCount), React schedules a re-render of the component. During the next render, useState will return the new, updated value.
Important: State updates are asynchronous and do not update the variable in the current execution context.
function AsynchronousState() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log(count); // This will log the OLD value, not the updated one!
};
return <button onClick={handleClick}>Increment</button>;
}
Functional Updates
If your new state depends on the previous state, you should pass a function to the updater function instead of the direct value. This guarantees you are working with the most up-to-date state, especially when multiple state updates occur rapidly.
function SafeCounter() {
const [count, setCount] = useState(0);
const handleIncrementThrice = () => {
// Bad: if count is 0, this results in 1, because count is 0 for all three calls in this closure
// setCount(count + 1);
// setCount(count + 1);
// setCount(count + 1);
// Good: passing a function guarantees we use the latest state value
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1); // Results in an increment of 3
};
return <button onClick={handleIncrementThrice}>+3</button>;
}
State Batching
React optimizes performance by batching state updates. If multiple state updates are triggered within the same event handler, React will group them together into a single re-render. In React 18 and newer, this batching is automatic across all events, including promises, timeouts, and native event handlers.
function BatchingExample() {
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const handleUpdateBoth = () => {
setName('Alice');
setAge(25);
// React will only re-render the component once here!
};
return <button onClick={handleUpdateBoth}>Update</button>;
}
Immutability in State
When storing objects and arrays in state, you must treat them as immutable. You cannot mutate the existing state directly. Instead, you must create a new object or array and pass it to the state setter.
Updating Objects
Use the spread operator (...) to copy existing properties while overriding specific ones.
function ProfileForm() {
const [user, setUser] = useState({ name: 'Bob', age: 30 });
const updateName = () => {
// WRONG: Mutating state directly
// user.name = 'Alice';
// setUser(user);
// CORRECT: Creating a new object
setUser({ ...user, name: 'Alice' });
};
}
Updating Arrays
Do not use array methods that mutate the array (like push, pop, splice). Use methods that return a new array (like map, filter, concat, or spread syntax).
function TodoList() {
const [todos, setTodos] = useState(['Learn React']);
const addTodo = (newTodo) => {
// WRONG: todos.push(newTodo); setTodos(todos);
// CORRECT:
setTodos([...todos, newTodo]);
};
const removeTodo = (indexToRemove) => {
// CORRECT: filter returns a new array
setTodos(todos.filter((_, index) => index !== indexToRemove));
};
}
Understanding how useState works, relying on functional updates for derived state, and strictly adhering to immutability are the cornerstones of writing bug-free React components.