react usestate is the most basic way to store changing data (state) inside a function component. A counter, a form field, whether a dropdown is open or closed… anything that changes through user interaction is state, and you need to hold it with useState so React can reflect the change on screen. Updating an ordinary variable won't trigger React; useState, on the other hand, both stores the value and re-renders the component when it changes.
How useState works
useState takes an initial value and returns a two-element array: the current value and the function that changes it. We name both at once with array destructuring:
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicked: {count}
</button>
);
}
Here count is the current value and setCount is the function that changes it. The 0 in useState(0) is only used on the first render; on later renders React remembers the previous value. The naming is entirely yours, but the [x, setX] convention is standard.
Never mutate state directly
The most common mistake in React is mutating state directly. The only way to change state is to call the set function. Assigning directly, like below, won't update the screen:
// WRONG — React won't notice this
count = count + 1;
// RIGHT — triggers a re-render
setCount(count + 1);
The same rule applies to objects and arrays. Instead of changing the existing object, you create a new object. The reason is that React detects changes by reference comparison (is the old value the same as the new one); if you mutate the same reference, React won't realize anything changed.
const [user, setUser] = useState({ name: "Ada", age: 30 });
// WRONG
user.age = 31;
setUser(user);
// RIGHT — a new object via spread
setUser({ ...user, age: 31 });
Functional updates: why and when?
Instead of passing a value directly to the set function, you can also pass a function that receives the previous state as its argument. This is called a functional update, and it's the safest method when the new value depends on the old one:
// depends on the previous value: prefer the functional form
setCount(prev => prev + 1);
The difference shows up when you update state several times in the same event. Compare these two examples:
// This increments the counter by only 1
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// This increments the counter by 3
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
In the first block count is the fixed value from that render (for example 0), so all three calls compute 0 + 1. In the functional form, each call receives the result of the previous update, so the result accumulates correctly.
Batching updates
React groups multiple state updates within the same event handler into a single render; this is called batching. So even if you make three separate set calls, the component re-renders once, not three times. This improves performance and prevents half-finished intermediate states from appearing on screen.
function handleClick() {
setOpen(true);
setCount(c => c + 1);
setMessage("updated");
// all three are applied in a single render
}
With React 18, this behavior applies not only to event handlers but also to setTimeout, Promises and other asynchronous callbacks (automatic batching). One important point: right after calling the set function, the state variable still holds its old value, because the update takes effect on the next render.
setCount(count + 1);
console.log(count); // still the old value — the render hasn't happened yet
Practical patterns with arrays and objects
The rule is the same with complex state: copy, change, set. Common patterns:
- Adding an item to a list:
setItems(prev => [...prev, newItem]) - Removing from a list:
setItems(prev => prev.filter(i => i.id !== id)) - Updating one item:
setItems(prev => prev.map(i => i.id === id ? { ...i, done: true } : i))
If your state branches heavily and contains many interdependent fields, useReducer may be more readable than useState. But for most everyday work, useState is sufficient and the simplest solution.
Frequently Asked Questions
What's the difference between useState and useRef?
useState re-renders the component when it changes; it's for data that needs to appear on screen. useRef stores a value across renders but does not trigger a render when it changes; it's used to access a DOM element or to hold a value that doesn't affect rendering.
Can I call useState inside a loop or condition?
No. All Hooks must be called at the top level of the component, in the same order on every render. Calling them conditionally or inside a loop breaks React's ability to match up states and throws an error.
What if the initial value is an expensive calculation?
Instead of passing the initial value directly, pass a function: useState(() => expensiveCalc()). This "lazy initializer" only runs on the first render and is not recomputed on later renders.
Setting up state management correctly is the foundation of a maintainable interface. If you're building a React-based project or want to improve an existing interface, get in touch with me — let's build a solid architecture together.