Props are read-only, but interactive UIs need to remember things β how many times a button was clicked, what a user typed, whether a menu is open. The useState hook is what gives a function component that memory.
1The Need for Memory
A plain JavaScript variable declared inside a function component is recreated from scratch on every render and never triggers a re-render when it changes β incrementing a local let count does nothing visible on screen. React needs a way to persist a value across renders and to know when that value changes so it can update the DOM.
That mechanism is called State. It's the component's own private memory, separate from the props it receives from its parent, and it's exactly what powers counters, form inputs, toggles, and anything else that needs to change over time in response to user interaction.
// We need memory
function Counter() {
return <button>Clicks: 0</button>;
}Static Variables Fail
Normal 'let' variables cannot trigger UI updates.
2Introducing useState
To give a function component memory, React provides a built-in Hook called useState, imported directly from the react package. Calling useState() tells React: remember this value across renders for this specific component instance, and re-render whenever it changes.
Hooks are just functions, but they follow a strict rule β they must be called at the top level of a component, never inside a conditional, loop, or nested function. This is what lets React reliably associate each useState call with the correct piece of memory across re-renders.
import { useState } from 'react';
function Counter() {
return <button>Clicks: 0</button>;
}Hooks: Tapping into React
3Array Destructuring
Calling useState() doesn't return a single value β it returns an array with exactly two items: the current state value, and a setter function used to update it. JavaScript's array destructuring syntax lets you extract both into named variables in a single line: const [value, setValue] = useState(initialValue).
The names on the left side are entirely up to you; React doesn't care what you call them. The convention of value/setValue (or count/setCount) is just a readability habit, not a requirement.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return <button>Clicks: {count}</button>;
}[ State, Setter ]
4Initialization Syntax
Putting it all together, a counter that starts at zero looks like const [count, setCount] = useState(0). The argument passed to useState β here, 0 β is the initial state, and React only uses it on the component's very first render.
On every subsequent re-render, React ignores that initial argument entirely and instead returns whatever the current stored value actually is, which may have been updated many times since the component first mounted.
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicks: {count}
</button>
);
}useState(InitialValue)
5Rendering State
Once destructured, a state variable behaves like any other JavaScript value inside JSX β wrap it in curly braces and React reads it straight from its internal memory. There's nothing special about the syntax; {count} works exactly the same way {someProp} would.
The part that IS special is what happens after the render: React re-runs this same rendering logic every time the state changes, so the curly-brace expression always reflects the latest stored value on screen.
// β This will only add 1, not 3!
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);Current count is: 5
6The Setter Function
You cannot change state by assigning a new value directly, like count = 1 β React has no way of detecting a plain variable reassignment, so nothing would re-render. Instead, you must call the setter function useState gave you: setCount(1).
Calling the setter does two things: it updates React's internal memory for that piece of state, and it schedules a re-render so the new value actually shows up on screen. Direct mutation silently does neither.
// β
Correct approach
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);Use the Setter!
Direct assignment breaks React.
7Triggering Re-renders
The most common pattern is wiring a setter to a user event, like onClick={() => setCount(count + 1)}. Passing an arrow function (rather than calling setCount immediately) means the update only fires when the button is actually clicked, not on every render.
This creates the core interaction loop every React app relies on: click, update state, re-render, show the new value β all handled automatically once you call the setter.
const [user, setUser] = useState({ name: 'Dan', age: 25 });
// β user.age = 26;
// β
setUser({ ...user, age: 26 });8The Snapshot Behavior
Here's a classic gotcha: state updates are not immediate. Calling setCount(count + 1) does not change the count variable in the currently-running render β if you console.log(count) on the very next line, it still prints the old value, because React queues the update for the next render instead of applying it synchronously.
This is why calling setCount(count + 1) three times in a row inside the same event handler only increases the count by one, not three β each call reads the exact same snapshot of count from that render.
// Ready for interactionState is a Snapshot
Queued for the next render.
9The Updater Function
Because of the snapshot behavior, whenever a new state value depends on the previous one, you should pass an updater function to the setter instead of a raw calculation: setCount(prev => prev + 1). React guarantees the prev argument reflects the most up-to-date state, even across multiple queued calls.
Calling setCount(prev => prev + 1) twice in the same handler now correctly increases the count by two, since each call's updater function receives the result of the previous one rather than a stale snapshot.
// Example
console.log("Running React...");setCount(prev => prev + 1)
10One State Variable or Many?
When a component has several related values, you can call useState once per value, or once with a single object holding all of them. Prefer several separate calls when the values update independently β it avoids spreading the rest of an object on every single update. Reach for one object only when the values are genuinely related and tend to change together, like a form's fields.
// Usually simpler: independent values
const [name, setName] = useState('');
const [age, setAge] = useState(0);
// One object only when fields change together
const [form, setForm] = useState({ name: '', age: 0 });Split or Combine?
Do these values change independently?
11Step-by-Step Breakdown
The Need for Memory. So far, our components have been static. They receive props from their parent, and they render UI. But props are strictly read-only. If we want a component to change *itself* over timeβlike a button that tracks how many times it was clicked, or a text input that tracks what the user is typingβthe component needs its own memory. In React, this memory is called 'State'.
Introducing useState. To give a functional component memory, we use a special React function called a 'Hook'. The most important hook in React is useState. By calling useState(), you are telling React: 'Hey, I want you to remember a value across renders for this specific component.' You must import it directly from the react package.
Array Destructuring. When you call useState(), it doesn't just return a single value. It returns an Array containing exactly TWO items: 1. The current value of the state. 2. A special 'Setter Function' used to update that value. We use JavaScript 'Array Destructuring' to extract both of these items and assign them to variables in a single, clean line of code.
Initialization Syntax. Let's put it all together. To create a state variable for a counter that starts at 0, you write const [count, setCount] = useState(0);. The argument you pass to useState (in this case, 0) is the 'Initial State'. React will only use this initial value the very first time the component renders.
Which of the following lines correctly initializes a state variable named theme with a starting string of 'dark'?
- βconst theme = useState('dark');
- βconst [theme, setTheme] = useState('dark');
- βlet [theme, setTheme] = State('dark');
Rendering State. Once you have destructured your state variable (count), you can use it inside your JSX exactly like any other JavaScript variable, such as a prop. Simply wrap it in curly braces. React will read the value from its internal memory and render it to the screen.
The Setter Function. How do we change the value? We CANNOT assign a new value directly (e.g., count = 1). We MUST use the Setter Function (setCount). When you call setCount(1), you are giving React an instruction: 'Update your internal memory for count to be 1, and then Re-Render the UI to show it.'
Triggering Re-renders. Let's hook the Setter Function up to a user event. By passing an arrow function to the button's onClick prop, we can call setCount(count + 1) every time the button is clicked. This creates a perfect loop: Click -> Update State -> Re-Render UI -> Show new Count.
If a component has const [score, setScore] = useState(0);, what is the correct way to update the score to 10 when a button is clicked?
- βonClick={ score = 10 }
- βonClick={ setScore(10) }
- βonClick={ () => setScore(10) }
The Snapshot Behavior. There is a massive 'gotcha' with React State. State updates are NOT immediate; they behave like snapshots. When you call setCount(count + 1), the count variable in the current render does NOT change. If you console.log(count) on the very next line, it will still print the old value! React queues the update for the *next* render.
The Updater Function. Because of the snapshot behavior, if your new state depends on the *previous* state (like adding +1), you should pass an 'Updater Function' to the setter instead of a raw value. setCount((prev) => prev + 1). React will pass the most up-to-date state into this function, ensuring safe, queued updates.
If count is currently 5, and you execute setCount(count + 1); setCount(count + 1); sequentially inside a single function, what will the count be on the next render?
- β7
- β6 (Because of snapshot behavior)
Object State. State doesn't have to be a simple number. You can store Strings, Booleans, Arrays, and Objects. However, when working with Objects or Arrays, there is a strict rule: You must treat them as IMMUTABLE. You cannot directly change a property inside a state object.
The Spread Operator. To safely update an object in state, you must construct a brand NEW object. Use the JavaScript Spread syntax (...) to copy all properties from the existing state object, and then overwrite the specific property you want to change.
One State Variable or Many?. When a component has several related values, you can either call useState once per value, or once with a single object holding all of them. Prefer several separate calls when the values update independently β it avoids having to spread the rest of the object on every update. Reach for one object only when the values are genuinely related and tend to change together, like a form's fields.
Mastery Achieved. Awesome! You now understand the core rules of React State. You've mastered useState initialization, destructuring, updater functions for safe calculation, and the vital rule of object immutability. You are ready to build fully interactive, data-driven applications!
Level Up π
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1State-Driven UI Changes Should Be Announced to Screen Readers
If a state change updates content that isn't near the user's current focus (like a live counter or a status message), pair the `setState` call with an `aria-live` region update so screen reader users are notified of the change, not just sighted users watching the screen.
2Controlled Inputs Backed by useState Still Need Proper Labels
A text input whose value is driven by `useState` (`value={text} onChange={...}`) is no different from any other input as far as accessibility goes β it still needs a real, programmatically associated `<label>`.
SEO Implications
- 1
Client-Side State Doesn't Exist in the Initial Server-Rendered HTML
A component's `useState` value starts at its initial argument during server-side rendering (e.g., with Next.js) β any content that only becomes visible after a state change driven by client interaction isn't present for a crawler evaluating the pre-hydration HTML.
- 2
Avoid Storing Critical Page Content Exclusively in useState
If SEO-relevant content should be visible immediately, don't gate it behind a state value that starts hidden and only becomes visible after a client-side effect or interaction β render it directly whenever possible.
Best Practices
Use the Updater Function Form Whenever New State Depends on Old State
`setCount(prev => prev + 1)` is safe regardless of how many times it's called in a row or how batched the updates are; `setCount(count + 1)` risks using a stale snapshot of `count` in certain call patterns.
Never Mutate Objects or Arrays Held in State
React detects state changes by reference comparison β mutating an object in place and passing the same reference back to the setter means React sees no change and won't re-render. Always construct a new object/array (e.g., via spread syntax).
Frequent Bugs
Calling the state setter multiple times in one handler only applies one update instead of several.
Each call read the same stale snapshot of the state variable from that render. Switch to the updater function form (`setCount(prev => prev + 1)`), which always receives the latest queued value.
Updating a property on a state object doesn't cause the component to re-render.
The object was mutated directly (`user.age = 26; setUser(user)`), so the reference passed back to `setUser` is identical to the one React already has β no change is detected. Construct a new object instead: `setUser({ ...user, age: 26 })`.
Real-World Examples
Controlled Text Input With useState
A search box keeps its current text in state, updating it on every keystroke and reading the latest value to drive a live filtered results list.
const [query, setQuery] = useState('');
<input value={query} onChange={(e) => setQuery(e.target.value)} />