1. Introduction đ
useState is the most fundamental Hook in React â it's usually the very first Hook developers learn. This tutorial takes a deep, focused look at exactly how useState works internally, covering initialization, updates, batching, and the patterns that separate clean state management from subtle, hard-to-debug issues.
Information
2. What is useState? đ¤
useState is a Hook that adds a piece of local, reactive state to a function component. It returns a pair: the current value and a function to update it.
Code Snippet
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}3. Why Use useState? đĄ
- Reactivity: Updating state automatically triggers a re-render with the new value.
- Persistence Across Renders: Unlike a regular variable, state survives between renders.
- Encapsulation: Each component instance maintains its own independent state.
- Simplicity: A minimal API for the most common state management needs.
4. Creating State đą
Code Snippet
const [value, setValue] = useState(initialValue);Calling useState declares a new independent state variable. The array destructuring syntax gives you full control over naming both the value and its setter.
Code Snippet
function ProfileForm() {
const [name, setName] = useState("");
const [age, setAge] = useState(18);
const [isSubscribed, setIsSubscribed] = useState(false);
}5. Reading State đ
The current state value behaves like a normal constant during a given render â it never changes mid-render, only between renders.
Code Snippet
function Greeting() {
const [name] = useState("Alice");
return <h1>Hello, {name}!</h1>;
}Note
6. Updating State âī¸
Calling the setter function schedules a state update and triggers a re-render for that component (and, if needed, its children).
Code Snippet
function Toggle() {
const [isOn, setIsOn] = useState(false);
return (
<button onClick={() => setIsOn(!isOn)}>
{isOn ? "ON" : "OFF"}
</button>
);
}Important
7. Initial State đ
The value passed into useState is used only on the first render. On every subsequent render, React ignores this argument entirely and returns the current stored state.
Code Snippet
function Timer() {
// "0" is only used once, on mount
const [seconds, setSeconds] = useState(0);
}8. Lazy Initialization đ¤
When computing the initial value is expensive, pass a function instead of a direct value â React calls it exactly once, on the first render only.
Code Snippet
function TodoApp() {
// â
Function only runs once
const [todos, setTodos] = useState(() => loadTodosFromStorage());
// â Runs on every render, even though the result is discarded after mount
// const [todos, setTodos] = useState(loadTodosFromStorage());
}Best Practice
9. Multiple State Variables đĸ
It's idiomatic in React to declare several independent useState calls rather than combining unrelated values into a single state object.
Code Snippet
function SignupForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [errors, setErrors] = useState({});
}Tip
10. Functional Updates đ
When the new state depends on the previous value, pass a function to the setter instead of a direct value â this guarantees correctness even with rapid or batched updates.
Code Snippet
function Counter() {
const [count, setCount] = useState(0);
function incrementThreeTimes() {
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
// Correctly results in +3
}
}Warning
11. Updating Objects đ§ą
Since state must remain immutable, updating an object means creating a brand-new object rather than modifying the existing one in place.
Code Snippet
function ProfileForm() {
const [user, setUser] = useState({ name: "Alice", age: 25 });
function updateName(newName) {
setUser({ ...user, name: newName });
}
}Danger
12. Updating Arrays đ
Code Snippet
function TodoList() {
const [todos, setTodos] = useState([]);
function addTodo(text) {
setTodos([...todos, { id: crypto.randomUUID(), text }]); // â
new array
}
function toggleTodo(id) {
setTodos(todos.map((todo) =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)); // â
new array, new object for the changed item
}
}Caution
13. Nested State đĒ
Updating deeply nested state requires copying every level of the structure that changes, since immutability must be preserved at each nested layer.
Code Snippet
function Settings() {
const [preferences, setPreferences] = useState({
theme: "light",
notifications: { email: true, sms: false },
});
function toggleSms() {
setPreferences({
...preferences,
notifications: {
...preferences.notifications,
sms: !preferences.notifications.sms,
},
});
}
}Tip
14. State Immutability đ
React determines whether to re-render by comparing the previous and next state references. Mutating state in place keeps the reference identical, so React has no way of detecting the change.
| Mutating (Avoid) | Immutable (Prefer) |
|---|---|
| obj.key = value | { ...obj, key: value } |
| array.push(item) | [...array, item] |
| array[i] = newVal | array.map((v, idx) => idx === i ? newVal : v) |
15. State Batching đ¯
React batches multiple state updates that happen within the same event handler into a single re-render, avoiding redundant work.
Code Snippet
function Form() {
const [name, setName] = useState("");
const [submitted, setSubmitted] = useState(false);
function handleSubmit() {
setName("Alice");
setSubmitted(true);
// Only ONE re-render occurs, not two
}
}Information
16. State Queue đĨ
When several updates are triggered before the next render, React processes them in order as a queue. Functional updates ensure each step in the queue sees the correctly updated value from the previous step.
Code Snippet
function QueueDemo() {
const [number, setNumber] = useState(0);
function handleClick() {
setNumber((n) => n + 1); // 0 -> 1
setNumber((n) => n + 1); // 1 -> 2
setNumber((n) => n * 2); // 2 -> 4
}
return <button onClick={handleClick}>{number}</button>; // results in 4
}17. Asynchronous State Updates âąī¸
State updates are not applied immediately â the current render's variable keeps its original value until the component re-renders with the new state.
Code Snippet
function Example() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
console.log(count); // logs the OLD value, not the updated one
}
}Caution
18. Derived State đ§Ž
Not every piece of UI data needs its own useState. If a value can be calculated from existing state or props, compute it directly during render instead of storing a redundant copy.
Code Snippet
function Cart({ items }) {
// â
Derived directly â always in sync with 'items'
const total = items.reduce((sum, item) => sum + item.price, 0);
return <p>Total: ${total.toFixed(2)}</p>;
}Best Practice
19. Resetting State đ
Changing a component's key prop forces React to treat it as an entirely new instance, discarding all of its previous state and starting fresh.
Code Snippet
function ProfileEditor({ userId }) {
return <Form key={userId} />;
// Changing userId fully resets Form's internal useState values
}Tip
20. Preserving State đž
React preserves a component's state across re-renders as long as it stays at the same position in the tree with the same type and key.
Code Snippet
function App({ showForm }) {
return (
<div>
{showForm && <SignupForm />}
{/* SignupForm's state resets each time it's unmounted and remounted */}
</div>
);
}Note
21. Sharing State đ
A single useState call is local to one component instance. To share state between siblings, it must be lifted to their closest common parent, or managed via Context.
Code Snippet
function Parent() {
const [selectedTab, setSelectedTab] = useState("home");
return (
<>
<TabBar selected={selectedTab} onSelect={setSelectedTab} />
<TabContent tab={selectedTab} />
</>
);
}22. Lifting State Up âŦī¸
Lifting state up means moving a useState call from a child component to a shared parent, then passing the value and setter down as props.
Code Snippet
// Before: state trapped in a single child, siblings can't access it
function Child() {
const [value, setValue] = useState("");
}
// After: lifted to the parent, shared between siblings
function Parent() {
const [value, setValue] = useState("");
return (
<>
<Input value={value} onChange={setValue} />
<Preview value={value} />
</>
);
}23. State Performance âĄ
- Placing state too high in the tree can cause unnecessary re-renders of unrelated child components.
- Split unrelated pieces of state into separate useState calls to limit re-render scope.
- Use useMemo for expensive derived computations based on state.
- Avoid storing large, frequently changing objects in a single useState if only a small part actually changes often.
Tip
24. Common State Patterns đ¨
Code Snippet
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen((prev) => !prev);Code Snippet
const [count, setCount] = useState(0);
const increment = () => setCount((c) => c + 1);
const decrement = () => setCount((c) => c - 1);
const reset = () => setCount(0);Code Snippet
const [formData, setFormData] = useState({ email: "", password: "" });
function handleChange(e) {
setFormData({ ...formData, [e.target.name]: e.target.value });
}25. TypeScript with useState đˇ
Code Snippet
// Inferred as boolean
const [isVisible, setIsVisible] = useState(false);
// Explicit generic for complex or nullable state
interface User {
id: string;
name: string;
}
const [user, setUser] = useState<User | null>(null);
// Explicit generic for a union of specific string values
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");Tip
26. Best Practices đ
- Keep each useState call focused on a single, independent piece of data.
- Use functional updates whenever new state depends on the previous state.
- Always treat state as immutable â create new objects and arrays rather than mutating.
- Use lazy initialization for expensive initial computations.
- Avoid storing values that can be derived from existing state or props.
- Colocate state close to where it's used to minimize unnecessary re-renders.
27. Common Mistakes đĢ
- Directly mutating state objects or arrays instead of creating new copies.
- Assuming state updates immediately, and reading the old value right after calling the setter.
- Calling the setter multiple times with a direct value instead of a functional update when relying on the previous state.
- Storing derived values in state instead of computing them during render.
- Passing an expensive function call directly to useState() instead of wrapping it for lazy initialization.
Danger
28. Frequently Asked Questions â
Question
Answer
Question
Answer
Question
Answer
29. Summary đ
useState is the foundation of interactivity in React function components, giving them the ability to remember and update data over time. Mastering its nuances â immutability, functional updates, batching, and lazy initialization â is essential for writing correct, performant React applications.