Learn how to make your React components interactive by managing dynamic data with the useState hook.
So far, our components have only displayed static data passed through props. But real applications need to respond to user actions — incrementing a counter, toggling a menu, filtering a list, typing in a search box. For this, we need state.
State is data that belongs to a component and can change over time. When state changes, React automatically re-renders the component to reflect the new data. This is the core of React's reactivity.
Consider this broken attempt at making a counter:
function Counter() {
let count = 0;
function handleClick() {
count = count + 1;
console.log(count); // This increments, but...
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}Clicking the button changes the count variable, but the screen never updates. Why? Because React does not know the variable changed — it has no reason to re-render the component.
To solve this, React provides the useState hook. Hooks are special functions that let you "hook into" React features from function components. useState is the most fundamental hook — it gives your component a piece of state that React tracks and re-renders when it changes.
The difference between props and state:
| Props | State |
|---|---|
| Passed from parent | Managed inside the component |
| Read-only (immutable) | Can be updated by the component |
| The parent controls them | The component controls it |
| Changing props re-renders the child | Changing state re-renders the component |
The useState hook is called inside your component function. It takes an initial value as an argument and returns an array with exactly two elements:
const [count, setCount] = React.useState(0);This uses array destructuring to name the two values. The convention is [something, setSomething].
Here is the working counter:
function Counter() {
const [count, setCount] = React.useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}When you call setCount(count + 1), React:
useState(0) returns the new value instead of 0.The initial value (0 in this case) is only used on the first render. On subsequent renders, useState returns the current state value.
Important rules for hooks:
// WRONG — hook inside a condition
function Bad() {
if (someCondition) {
const [value, setValue] = React.useState(0); // Never do this!
}
}
// CORRECT — always at the top level
function Good() {
const [value, setValue] = React.useState(0);
// Use conditions in the JSX or handler logic instead
}A critical concept in React: you must never mutate state directly. Always use the setter function to create a new value. React relies on detecting that the reference has changed to know when to re-render.
With primitive values (numbers, strings, booleans), this is natural:
const [count, setCount] = React.useState(0);
setCount(count + 1); // Creates a new number
const [name, setName] = React.useState('');
setName('Alice'); // Creates a new stringWith objects and arrays, you must create a new copy instead of modifying the existing one:
// WRONG — mutating the existing object
const [user, setUser] = React.useState({ name: 'Alice', age: 25 });
user.age = 26; // Mutating the object!
setUser(user); // React sees the same reference — no re-render!
// CORRECT — creating a new object with the spread operator
setUser({ ...user, age: 26 }); // New object, React re-rendersThe same applies to arrays:
const [items, setItems] = React.useState(['A', 'B']);
// WRONG
items.push('C');
setItems(items); // Same reference!
// CORRECT
setItems([...items, 'C']); // New arrayFunctional updates: When the new state depends on the previous state, use the callback form of the setter to avoid stale values:
// Basic form — fine for simple cases
setCount(count + 1);
// Functional update — safer, especially in async situations
setCount(prevCount => prevCount + 1);The functional form receives the most up-to-date previous state value, which prevents bugs when multiple updates happen in the same render cycle.
A component can have as many useState calls as it needs. Each one manages an independent piece of state:
function UserForm() {
const [name, setName] = React.useState('');
const [email, setEmail] = React.useState('');
const [age, setAge] = React.useState(0);
return (
<form>
<input value={name} onChange={e => setName(e.target.value)} />
<input value={email} onChange={e => setEmail(e.target.value)} />
<input
type="number"
value={age}
onChange={e => setAge(Number(e.target.value))}
/>
</form>
);
}Each state variable is completely independent — updating name does not affect email or age.
When to use one state vs. multiple:
const [position, setPosition] = React.useState({ x: 0, y: 0 });
// Update both at once:
setPosition({ x: event.clientX, y: event.clientY });State vs. regular variables:
Not all data needs to be state. Use state only for data that:
const.function Cart({ items }) {
// Derived value — NOT state, computed from props
const total = items.reduce((sum, item) => sum + item.price, 0);
// This IS state — changes in response to user action
const [couponCode, setCouponCode] = React.useState('');
return <p>Total: ${total}</p>;
}<div id="root"></div>
<script src="https://unpkg.com/react@19/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@19/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
function Counter() {
const [count, setCount] = React.useState(0);
return (
<div style={{ textAlign: "center", padding: "20px" }}>
<h2>Counter: {count}</h2>
<button onClick={() => setCount(count - 1)}>- Decrease</button>
{" "}
<button onClick={() => setCount(0)}>Reset</button>
{" "}
<button onClick={() => setCount(count + 1)}>+ Increase</button>
</div>
);
}
function App() {
return <Counter />;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>Why does directly mutating a state variable (like `count = count + 1`) not update the UI?