useState
The useState hook is the most fundamental React hook. It allows functional components to declare, read, and update state variables. State represents data that changes over time and affects what the component renders.
Basic Usage
useState returns an array with exactly two items:
- The current state.
- A set function that lets you update the state to a different value and trigger a re-render.
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0); // 0 is the initial state
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Functional Updates (Updater Function)
When calculating the next state depends on the previous state, you should pass a function to the setter instead of the direct value. This ensures you are always working with the most up-to-date state, especially when multiple state updates occur rapidly or asynchronously.
export default function SafeCounter() {
const [count, setCount] = useState(0);
const incrementThrice = () => {
// If we used setCount(count + 1) three times, it would only increment by 1
// because 'count' is captured from the current render closure.
// Using functional updates ensures the previous state is passed sequentially.
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
};
return <button onClick={incrementThrice}>+3</button>;
}
Lazy Initialization
If your initial state requires an expensive computation (e.g., parsing a large JSON string or reading from localStorage), you can pass a function to useState. This function will only run once during the initial render, preventing the expensive operation from happening on subsequent re-renders.
import { useState } from 'react';
function getInitialTodos() {
const savedTodos = localStorage.getItem('todos');
return savedTodos ? JSON.parse(savedTodos) : [];
}
export default function TodoApp() {
// Pass the function itself, do not call it: getInitialTodos (Not getInitialTodos())
const [todos, setTodos] = useState(getInitialTodos);
// ...
}
State Batching in React 18
React groups multiple state updates into a single re-render to optimize performance. This is known as batching. Before React 18, batching only occurred inside React event handlers (like onClick).
With React 18's Automatic Batching, updates inside asynchronous operations (like Promises, setTimeout, or native event handlers) are also batched automatically.
import { useState } from 'react';
export default function BatchingExample() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
const handleClick = () => {
fetch('/api/data').then(() => {
// In React 18, these two updates will only trigger ONE re-render.
setCount(c => c + 1);
setFlag(f => !f);
});
};
return <button onClick={handleClick}>Fetch & Update</button>;
}
Important Considerations
- State is immutable: Never mutate state directly (e.g.,
state.list.push(item)). Always create a new object or array and pass it to the setter function. - Asynchronous nature: Calling the set function does not change the state variable in the currently executing code. It schedules a re-render with the new state.