State is internal, mutable data owned by a component — updates must be immutable (creating new objects, not mutating existing ones) and are batched asynchronously in React 18+ for performance.
Returns [value, setter] — setter accepts a new value or updater function. Use updater form (prev => ...) when new state depends on previous state.
Never mutate state directly — always create new objects/arrays. React uses reference equality (===) to detect changes.
React 18+ batches all state updates into a single re-render — multiple setState calls in the same handler cause one re-render, not multiple.
When siblings need shared data, move state to their closest common parent and pass it down as props.
If a value can be computed from existing state or props, calculate it during render instead of storing it separately.
State represents data that changes over time within a component. When state updates, React re-renders the component and its children to reflect the new data. Understanding state management is fundamental to building interactive React applications.
const [count, setCount] = useState(0) declares a state variable with an initial value. The hook returns a pair: the current value and a setter function. The setter can accept a new value directly (setCount(5)) or an updater function that receives the previous state (setCount(prev => prev + 1)). Use the updater form when the new state depends on the previous state — this avoids stale closures.
React state must never be mutated directly. state.items.push(newItem) is wrong — React won't detect the change because the array reference is the same. Instead, create new objects and arrays: setItems([...items, newItem]) for adding, setItems(items.filter(item => item.id !== id)) for removing, setItems(items.map(item => item.id === id ? { ...item, name: 'New' } : item)) for updating.
Why immutability? React uses reference equality (===) to determine if state changed. A mutated object has the same reference, so React skips the re-render. A new object has a different reference, so React knows to update. This also enables features like React.memo, useMemo, and time-travel debugging.
In React 18+, all state updates are automatically batched — multiple setState calls in the same event handler, timeout, or promise are combined into a single re-render. Before React 18, only updates inside React event handlers were batched. This means calling setA(1); setB(2); setC(3) in a handler causes one re-render, not three.
When two sibling components need access to the same data, move the state to their closest common parent. The parent holds the state and passes it down as props. This is a core React pattern — state should live in the lowest common ancestor that needs it.
If a value can be computed from existing state or props, don't store it in state. Calculate it during render instead. const fullName = firstName + ' ' + lastName is better than storing fullName as separate state that you must keep in sync. This eliminates synchronization bugs.
When state logic is complex (multiple sub-values, state transitions that depend on previous state), useReducer is cleaner than multiple useState calls. const [state, dispatch] = useReducer(reducer, initialState) — the reducer function takes current state and an action, returns new state. Same pattern as Redux but component-local.
| | State | Props | |---|---|---| | Owner | Component itself | Parent component | | Mutable | Yes (via setter) | No (read-only) | | Triggers re-render | Yes | Yes | | Purpose | Internal, changing data | External configuration |
State is internal and mutable (via setters, never direct mutation). Props are external and read-only. State updates are batched and asynchronous in React 18+. Always use immutable update patterns — React relies on reference equality to detect changes. If a value can be derived from existing state, don't store it separately.
Fun Fact
Before React 18, state updates inside setTimeout, fetch callbacks, and native event listeners were NOT batched — each setState caused its own re-render. React 18's automatic batching was a breaking change that unified behavior across all contexts, and it's one of the main reasons the upgrade was worth it.