Props give a component data from its parent, but they're read-only — state is what lets a component remember and change its own data over time. This lesson covers the useState hook, the setter function that triggers re-renders, and the rules that keep state updates safe and predictable.
1What is State?
If props are the data a component receives from its parent, state is the memory a component keeps for itself. It lets a component remember things across renders — a typed-in value, a toggled menu, a fetched API response — and, unlike props, it's meant to be updated over time.
This internal memory is what turns a static render into something that can respond to user actions or network events, without ever needing a parent to pass in new data.
// React State: The Component's MemoryComponent Memory
2The useState Hook
Function components gain state by calling the useState Hook, imported from react. A Hook is simply a special function that lets a component tap into React's internal features — in this case, reserving a slot in React's memory to hold a value across renders.
The single argument passed to useState is the initial state value, and React only ever uses it once, on the component's very first render — every render after that reads whatever the state has since been updated to.
import { useState } from 'react';
const [count, setCount] = useState(0);Initial State Mounted
3The Array Return
Calling useState returns exactly two items packed into an array: the current state value, and a setter function used to change it. ES6 array destructuring — const [count, setCount] = useState(0) — lets you pull both out in a single line.
By convention, the setter is named by prefixing set onto the state variable's name (count/setCount, name/setName), which makes the relationship between a value and its updater instantly readable.
<button onClick={() => setCount(count + 1)}>
Count is {count}
</button>count
0
setCount
f()
4Triggering a Re-render
Calling the setter function is what tells React the data has changed and the UI needs updating — it's the only mechanism that does this. When you call it, React re-runs your entire component function from top to bottom with the new state value, a process known as re-rendering.
Nothing else — not a plain variable reassignment, not a mutation — will trigger this; the setter function is the sole trigger for React to reconcile the UI with new data.
// ❌ NEVER DO THIS:
count = count + 1;
// ✅ ALWAYS DO THIS:
setCount(count + 1);React waits for the click to trigger a re-render.
5Never Mutate State Directly
Directly reassigning a state variable, like count = count + 1, changes the value in memory but tells React nothing — React only finds out about changes through the setter function, so a direct mutation leaves the UI silently stuck on stale data.
The rule is absolute: treat state as immutable and always go through the setter (setCount(count + 1)) so React knows to re-render with the new value.
const [name, setName] = useState('Dev');
const [items, setItems] = useState([]);Mutation = Stale UI
Always use the setter function.
6Any Data Type
A state variable isn't restricted to numbers or strings — it can hold any valid JavaScript value: booleans, arrays, nested objects, even null. That flexibility is what lets a single useState call manage something as complex as a shopping cart array or a nested user profile object.
Each separate useState call in a component produces its own independent piece of state, so a component can freely mix multiple simple and complex state values without them interfering with each other.
setCount((prevCount) => prevCount + 1);User: Dev
Cart Items: 0
7Functional Updates
When a new state value depends on the previous one, relying on the state variable directly (setCount(count + 1)) is risky, because state updates are scheduled rather than instant — count can be stale if several updates happen in quick succession.
The safer pattern is a functional update: passing a function to the setter, like setCount((prevCount) => prevCount + 1). React guarantees prevCount reflects the most current queued value, so this pattern stays correct no matter how many updates are batched together.
setCount(count + 1);
console.log(count); // Still shows the old value!Safe Counter
prevCount (5) ➔ 6
8State is a Snapshot (Async)
State updates in React are asynchronous: calling a setter queues the update for the next render rather than changing the variable immediately. If you console.log the state variable on the very next line after calling its setter, you'll still see the old value.
This is because a state variable behaves like a snapshot frozen for that specific render — it only reflects the new value once React has actually re-rendered the component with the update applied.
/* Lifting State Up Diagram */
// Parent State -> Child 1 & Child 2> console.log(count)
0
9Lifting State Up
State is local to whichever component defines it, which becomes a problem the moment two sibling components need to share the same data — React's data flow is strictly top-down, so you can't pass values sideways between siblings.
The fix is to 'lift the state up': move the useState call into their closest common parent component, then pass both the current value and the setter function down to each sibling as props, so both can read and update the same shared piece of state.
// ❌ WRONG:
if (val) { useState(0); }
// ✅ RIGHT:
const [s, setS] = useState(0);State: shared
Reads
Updates
10Rules of Hooks
Hooks come with a strict rule: they must always be called at the top level of a component function, never inside a loop, an if statement, or a nested function. React depends on hooks being called in the exact same order on every render to correctly match each hook call to its corresponding piece of state.
Conditionally calling a hook — even just skipping one call under certain conditions — breaks that ordering and corrupts React's internal state tracking for the rest of the component.
/* State Lab: Multi-Counter Rendered */Top Level Only
Hooks cannot be nested.
11Live Rendering Lab
Watching two independent counters side by side makes the isolation of state concrete: clicking one counter's button updates only that counter's own internal state, instantly refreshing just its piece of the UI, while the other counter remains completely untouched.
This is React's declarative model in action — you never manually touch the DOM; you update state, and React figures out exactly which part of the UI needs to change to match it.
setUser(prev => ({ ...prev, age: 30 }));12Spreading Complex Objects
When state holds an object, calling the setter replaces the entire object — React does not automatically merge in just the fields you changed. Updating a single property like age while keeping the rest of the object intact requires spreading the old properties into a new object first.
The pattern setUser(prev => ({ ...prev, age: 30 })) copies every existing field from prev, then overwrites only age, producing a brand-new object reference that React can detect as changed.
/* Next: Effect Lifecycle */name: 'Dev',
age: 30
}
13Step-by-Step Breakdown
What is State?. Welcome to State Management. If props are the external data passed into a component from its parent, State is the component's own internal memory. It allows components to 'remember' things between renders, such as user inputs, network responses, or UI toggles. Unlike props, which are strictly read-only, state is meant to be updated over time in response to user actions or network events.
The useState Hook. To add state to a functional component, we must use the 'useState' Hook. A Hook is a special function that lets you 'hook into' React's internal features. When you call useState, it reserves a spot in React's memory for your variable. You pass the initial state value as the only argument. This initial value is only ever used during the very first render of the component.
The Array Return. The useState hook returns exactly two things inside an array. We use ES6 array destructuring to grab them instantly. The first item ('count') is the current state value. The second item ('setCount') is a setter function that lets you update the state. By convention, we always name the setter function by prefixing 'set' to the name of the state variable.
When initializing state using const [value, setValue] = useState(false);, what is the specific role of the setValue variable?
- →To get the current value
- →A function to update the value
Triggering a Re-render. Here is the most important concept in React: When you call the setter function, React completely destroys the current UI and re-runs your entire component function from top to bottom with the new state value. This process is called 'Reconciliation' or 'Re-rendering'. The setter function is the ONLY way to tell React that the data has changed and the UI needs an update.
Never Mutate State Directly. Because React relies entirely on the setter function to know when to update the DOM, you must NEVER modify the state variable directly (e.g., count = count + 1). If you mutate it directly, the variable changes in memory, but React remains completely unaware. Your UI will become 'stuck' and will not reflect the new data. Always, always treat state as immutable and use the setter.
Any Data Type. State isn't limited to just numbers or strings. A state variable can hold any valid JavaScript data type: booleans, arrays, nested objects, or even null. This allows you to manage complex UI states, like a list of shopping cart items or a deeply nested user profile object. However, remember that each piece of state is completely independent.
Functional Updates. When you need to update state based on its previous value (like adding 1 to a counter), it's dangerous to rely on the count variable directly. Because state updates are scheduled, count might be stale if multiple updates happen quickly. The safest pattern is to pass a function to the setter. React will guarantee that prevCount is the most up-to-date value before applying the logic.
State is a Snapshot (Async). A huge gotcha for beginners: React state updates are asynchronous. When you call a setter, React queues the update for the next render; it doesn't change the variable immediately. If you try to console.log your state right after calling the setter, you will see the old, stale value! State variables act like a snapshot of the UI for that specific render cycle.
You are building a shopping cart. The user clicks 'Add to Cart' 3 times very fast. To ensure the count is exactly accurate and not based on a stale closure, which pattern MUST you use?
- →setCart(cart + 1)
- →setCart(prev => prev + 1)
Lifting State Up. State is strictly local and isolated to the component where it is defined. But what if two sibling components need to share the same data? Because React data flow is strictly top-down, you cannot pass data sideways. You must 'Lift the State Up'. This means moving the useState hook into their closest common Parent component, and then passing both the state and the setter function down as props.
Rules of Hooks. There are strict rules for using Hooks. You must ALWAYS call hooks at the Top Level of your React function. Never call a hook inside a loop, an if-statement, or a nested JavaScript function. React relies on the exact order in which hooks are called to associate the correct state with the correct variable. Breaking this rule completely destroys React's internal state tracking.
Live Rendering Lab. Observe the render on the right. Notice how clicking any specific counter button updates its own internal state, causing only that specific UI fragment to change instantly. The states are completely independent of each other. This is the power of React: declarative, data-driven interfaces where the DOM automatically matches your state.
Spreading Complex Objects. When your state is an object, remember that React replaces the ENTIRE object when you call the setter. It does NOT automatically merge fields for you! If you want to update just the 'age' property of a user object, you must SPREAD (...) the old object properties into the new object first, and then overwrite the specific field.
True or False: When you call setState(5), React halts the code execution and immediately synchronously updates the DOM to show the new value.
- →True
- →False (It is asynchronous)
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 Toggles Need Their ARIA State Kept in Sync
When state controls a UI pattern like an expandable menu or accordion, update the corresponding `aria-expanded` or `aria-pressed` attribute alongside the state value so assistive technology reflects the same open/closed state sighted users see.
2Loading States Should Be Announced, Not Just Shown
If a `useState` boolean is used to show a spinner while data loads, pair it with an `aria-live` region or `aria-busy` attribute so screen reader users know a background operation is happening, not just users who can see the spinner.
SEO Implications
- 1
Content Gated Behind Client-Only State Is Invisible Pre-Hydration
A `useState` value starts at its initial argument during server rendering — any content that only appears after a state change triggered by user interaction is absent from the HTML a crawler evaluates before JavaScript runs.
- 2
Excessive Re-renders From Poorly Structured State Can Slow Time-to-Interactive
Storing overly granular or redundant state that triggers frequent re-renders can delay when a page becomes responsive, which search engines factor into their ranking of page experience.
Best Practices
Use Functional Updates When New State Depends on Old State
`setCount(prev => prev + 1)` is safe regardless of how many updates are queued in a row; `setCount(count + 1)` risks reading a stale snapshot of `count` if called multiple times before a re-render.
Keep State as Minimal and Normalized as Possible
Don't store derived values (like a filtered list) in their own state when they can be computed directly from existing state during render — duplicated state is a common source of bugs where the two copies drift out of sync.
Frequent Bugs
The UI doesn't update after a state object's property is changed.
The object was mutated in place (`user.age = 26; setUser(user)`) instead of replaced, so React sees the same reference and detects no change. Construct a new object instead: `setUser({ ...user, age: 26 })`.
A value logged immediately after calling its setter still shows the old data.
State updates are asynchronous and queued for the next render — the variable in the current render's closure never changes. Read the updated value from the next render (or from the functional updater's argument) instead of the line right after the setter call.
Real-World Examples
Toggle Menu Driven by Boolean State
A dropdown menu's open/closed state is tracked with a single boolean, flipped by a functional update on each click of its trigger button, and used to conditionally render the menu contents.
const [isOpen, setIsOpen] = useState(false);
<button onClick={() => setIsOpen(prev => !prev)}>Menu</button>
{isOpen && <DropdownContent />}