While Props allow parent components to pass data downward, State is the internal memory of a component. State allows React components to remember information between user interactions—such as text entered into an input box, the current page number of a paginated table, or whether a dropdown menu is open.
In modern React, local state is declared using the built-in useState hook. You import it directly from the react package:
import React, { useState } from 'react';
function SimpleCounter() {
// Array Destructuring: [currentValue, setterFunction] = useState(initialValue)
const [count, setCount] = useState(0);
return (
<div className="counter-card">
<h3>Current Count: {count}</h3>
<button onClick={() => setCount(count + 1)}>
Increment (+1)
</button>
<button onClick={() => setCount(0)} style={{ marginLeft: '10px' }}>
Reset
</button>
</div>
);
}
export default SimpleCounter;
useState(0): Initializes the state variable with 0 on the component's initial mount.count: The current state value during this render cycle.setCount: The setter function. Invoking it notifies React to re-render the component with the new value.Because React batches state updates together for performance, calculating next state based on the current state variable can occasionally lead to stale values if triggered repeatedly in a single event handler. Always pass an updater callback when next state depends on prior state:
function BatchCounter() {
const [count, setCount] = useState(0);
const handleTripleAdd = () => {
// ❌ Problematic: count is 0 in all three lines! Results in count = 1
// setCount(count + 1);
// setCount(count + 1);
// setCount(count + 1);
// ✅ Clean & Correct: Uses functional updater; Results in count = 3
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
};
return <button onClick={handleTripleAdd}>Triple Increment (+3)</button>;
}
In JavaScript, objects and arrays are reference types. When updating an object in React state, you must not mutate the object directly. Instead, create a new object using the ES6 Spread operator (...):
function UserProfileForm() {
const [user, setUser] = useState({
name: 'Rohan',
city: 'New Delhi',
role: 'Editor'
});
const updateCity = (newCity) => {
// ❌ WRONG: user.city = newCity; (React will NOT detect change!)
// ✅ CORRECT: Copy existing properties, then overwrite 'city'
setUser(prevUser => ({
...prevUser,
city: newCity
}));
};
return (
<div>
<p>Name: {user.name} | City: {user.city} | Role: {user.role}</p>
<button onClick={() => updateCity('Bengaluru')}>
Relocate to Bengaluru
</button>
</div>
);
}
Similarly, avoid mutator methods like push(), pop(), or splice(). Use non-mutating methods like concat(), filter(), map(), or the spread syntax:
function TodoList() {
const [todos, setTodos] = useState(["Study React", "Build Projects"]);
// 1. Add item immutably
const addTodo = (text) => {
setTodos(prev => [...prev, text]);
};
// 2. Remove item immutably
const removeTodo = (indexToDelete) => {
setTodos(prev => prev.filter((_, idx) => idx !== indexToDelete));
};
return (
<ul>
{todos.map((item, idx) => (
<li key={idx}>
{item}
<button onClick={() => removeTodo(idx)}>✕</button>
</li>
))}
</ul>
);
}
Now that your components can maintain internal state, let us learn how to handle user clicks, keystrokes, and form events in React Event Handling & Synthetic Events.
Accelerate your React mastery by studying these tightly connected core tutorials:
Updating state on button clicks and input changes.
Managing complex interdependent state transitions.
Binding input elements directly to React state.
Scaling state beyond single components cleanly.