React useState Hook: Managing State
Web DevelopmentLesson 3 of 64 min read
What you'll learn
- Add state to a component with
useStateand update it correctly - Update objects and arrays immutably instead of mutating them
- Explain why state updates are batched and when to use the functional form
- Lift state up so sibling components can share it
On this page
A normal variable inside a component resets on every render and changing it does not update the screen. State is how a component remembers things between renders and tells React to redraw when they change. It is what turns a static page into an app.
useState basics: a counter
useState(initialValue) returns a pair: the current value and a function to update it. By convention you name them thing and setThing. Calling the setter schedules a re-render, and during that re-render useState returns the new value.
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}Output
Initially: You clicked 0 times After 3 clicks: You clicked 3 times After Reset: You clicked 0 times
Hooks follow two rules. Call them only at the top level of a component (not inside loops, conditions or nested functions), and only from components or other hooks. React identifies each useState call by its position, so the order must be identical on every render.
State is a snapshot: updates are batched
Each render sees a fixed snapshot of state. Calling setCount(count + 1) does not change the count variable in the running function; it asks React for a new render with a new value. React also batches several updates from one event into a single render for performance.
This surprises almost everyone. The handler below looks like it adds 3, but it adds 1, because all three calls read the same snapshot value of count.
function addThreeWrong() {
setCount(count + 1); // count is 0 in this snapshot -> asks for 1
setCount(count + 1); // asks for 1 again
setCount(count + 1); // asks for 1 again
console.log(count); // still 0, not 3
}
function addThreeRight() {
setCount((c) => c + 1); // c is the latest pending value
setCount((c) => c + 1);
setCount((c) => c + 1); // result: 3
}Output
After clicking addThreeWrong from 0: count becomes 1 After clicking addThreeRight from 0: count becomes 3
Objects and arrays: never mutate, always copy
React decides whether state changed by comparing the old and new value with Object.is. If you mutate an array or object in place and pass the same reference to the setter, React sees no difference and may skip the update. Treat state as immutable: create a new object or array and pass that.
import { useState } from "react";
type Todo = { id: number; text: string; done: boolean };
export default function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([
{ id: 1, text: "Learn useState", done: false },
]);
const [text, setText] = useState("");
function addTodo() {
if (text.trim() === "") return;
// new array: old items plus the new one
setTodos([...todos, { id: Date.now(), text: text.trim(), done: false }]);
setText("");
}
function toggleTodo(id: number) {
// map returns a new array; changed item is a new object
setTodos(todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
}
function removeTodo(id: number) {
setTodos(todos.filter((t) => t.id !== id));
}
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button onClick={addTodo}>Add</button>
<ul>
{todos.map((t) => (
<li key={t.id}>
<span style={{ textDecoration: t.done ? "line-through" : "none" }}>
{t.text}
</span>
<button onClick={() => toggleTodo(t.id)}>Toggle</button>
<button onClick={() => removeTodo(t.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}Output
[ input box ] [Add] - Learn useState [Toggle] [Delete] Typing "Buy milk" and pressing Add appends: - Buy milk [Toggle] [Delete]
The same idea applies to objects. To change one field of a profile object, spread the old one and override the field: setProfile({ ...profile, city: "Pune" }). For nested objects, copy each level you change. If that gets painful, keep state flatter or split it into several useState calls.
Lifting state up
When two components need the same data, move the state to their closest common parent and pass it down as props, with a callback for changes. In a shopping cart, the product list adds items and the header badge shows the count, so the cart state lives in the parent that contains both.
import { useState } from "react";
function ProductList({ onAdd }: { onAdd: (name: string) => void }) {
const products = ["Keyboard", "Mouse", "Monitor"];
return (
<ul>
{products.map((p) => (
<li key={p}>
{p} <button onClick={() => onAdd(p)}>Add to cart</button>
</li>
))}
</ul>
);
}
function CartBadge({ count }: { count: number }) {
return <span>Cart ({count})</span>;
}
export default function Shop() {
const [cart, setCart] = useState<string[]>([]);
return (
<div>
<CartBadge count={cart.length} />
<ProductList onAdd={(name) => setCart([...cart, name])} />
</div>
);
}Output
Cart (0) Keyboard [Add to cart] Mouse [Add to cart] Monitor [Add to cart] After clicking Mouse twice the header reads: Cart (2)
Keep state as close as possible to where it is used, and lift it only when siblings need to share it. Lifting too early makes parents noisy; lifting too late leads to duplicated, out-of-sync copies of the same data.
Common mistakes
- Mutating state directly.
todos.push(item); setTodos(todos);keeps the same array reference. Symptom: the list does not update, or updates only after some unrelated re-render. Always create a new array. - Reading state right after setting it.
setCount(5); console.log(count);logs the old value. Compute the next value in a variable if you need it immediately. - Stale values in timers. A
setIntervalcreated once that usessetCount(count + 1)increments only once, because it keeps the first snapshot. UsesetCount((c) => c + 1). - Storing derived data in state. Keeping both
todosanddoneCountin state invites them to disagree. Calculatetodos.filter(t => t.done).lengthduring render instead. - Calling hooks conditionally.
if (loggedIn) { useState(0) }breaks hook ordering. Symptom: the error "Rendered more hooks than during the previous render".
How this shows up in interviews and real projects
Interviewers love to ask why setState is not synchronous, how you would update an item in an array, and why mutating state is a bug. Being able to explain the snapshot model and write the map/spread update from memory covers most of these. In real projects the same patterns appear in carts, forms, filters and toggles. Next, React Events and Forms: Controlled Inputs shows how state and user input work together.
Try it yourself
Build a profile editor: an object in state with name and city, two inputs that update it, and a line below showing "Asha lives in Pune" that updates live. Then add a "Skills" list where you can add and remove skills from an array in state.
Show a hint
Use setProfile({ ...profile, name: e.target.value }) for the object and [...skills, newSkill] / skills.filter(...) for the array. Do not keep a separate state variable for the summary line; build it during render.
Key takeaways
useStategives a component memory; calling the setter schedules a re-render with the new value.- Each render sees a snapshot of state, and multiple updates in one event are batched together.
- Use the functional form
setX((prev) => ...)when the next value depends on the previous one. - Never mutate state: copy arrays and objects with spread,
mapandfilter. - Lift state to the nearest common parent when siblings need to share it.
Test yourself
A trick from this lesson's ride: A plain variable is a whiteboard wiped each render; state is a notebook React keeps. Four more tricks like it wait along the route, each followed by a question.
Frequently asked questions
Why is my React state not updating immediately?
State updates are scheduled and batched, and each render uses a fixed snapshot of state. The variable in your running function keeps its old value until React re-renders. If you need the next value right away, store it in a local variable, or use the functional update form when it depends on the previous value.
How do I update an array in React state?
Create a new array instead of changing the old one. Use spread to add ([...items, newItem]), filter to remove, and map to change one element. Pass the new array to the setter so React sees a different reference.
What does lifting state up mean in React?
It means moving state from a child to the closest common parent of the components that need it, then passing the value down as props and changes up through callback props. This keeps one source of truth instead of duplicated copies that can drift apart.