Chapter 05 - Let's Get Hooked!
Chapter 05 - Let's Get Hooked!
Hey everyone! Welcome back to Namaste React!
In the last chapter we built the UI. But our search bar and filter buttons don't actually work yet. Today we learn useState — the hook that gives React components memory and makes them interactive!
What we will cover:
- What is State? Why do we need it?
- useState — syntax and how it works
- How useState triggers re-renders
- useState vs regular variables
- Functional update form — prev => prev + 1
- Controlled inputs with useState
- Live search/filter feature
- useState with objects and arrays
- The stale closure problem
- Interview Questions
1. What is State? Why Do We Need It?
THE PROBLEM — Regular variables don't cause re-renders:
=========================================================
// ❌ This does NOT work! Click the button — nothing happens on screen!
function Counter() {
let count = 0;
function handleClick() {
count++; // count changes in memory
console.log(count); // console shows updated count
// BUT: React doesn't know count changed!
// React does NOT re-render the component!
// The screen stays at 0 forever!
}
return (
<div>
<p>Count: {count}</p> {/* Always shows 0! */}
<button onClick={handleClick}>Increment</button>
</div>
);
}
WHY DOESN'T IT WORK?
→ React renders a component and "takes a snapshot"
→ JSX is evaluated once using the current values
→ When count changes in a regular variable, React has NO IDEA
→ React never re-runs the component function
→ The screen never updates!
┌─────────────────────────────────────────────────────────────┐ │ WHAT IS STATE? │ ├─────────────────────────────────────────────────────────────┤ │ │ │ State = A variable that React WATCHES. │ │ │ │ When state changes: │ │ 1. React detects the change │ │ 2. React re-renders the component │ │ 3. New JSX is returned with updated values │ │ 4. React updates the real DOM │ │ 5. User sees the new UI! │ │ │ │ State is the component's MEMORY. │ │ It persists across renders (unlike regular variables │ │ which reset on every render!). │ │ │ └─────────────────────────────────────────────────────────────┘
2. useState — Syntax and How It Works
// useState SYNTAX:
const [stateVariable, setterFunction] = useState(initialValue);
// Breaking it down:
// - useState() : React Hook (must import from 'react')
// - initialValue : the starting value (runs only ONCE on mount)
// - stateVariable : current value (read-only! never mutate directly)
// - setterFunction : call this to UPDATE the state and trigger re-render
import React, { useState } from "react";
function Counter() {
const [count, setCount] = useState(0); // initial value = 0
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
HOW useState WORKS INTERNALLY (connection to Deep Dive 01):
============================================================
On FIRST render:
1. useState(0) is called
2. React creates a slot in the Fiber node's hook linked list
3. Stores initialValue = 0 in that slot
4. Returns [0, setCount]
On BUTTON CLICK → setCount(1):
1. React queues an update: { lane: DefaultLane, action: 1 }
2. React schedules a re-render of this component
On RE-RENDER:
1. Component function runs again
2. useState(0) is called AGAIN
3. But React IGNORES the initialValue (0) this time!
4. React reads the stored value from the fiber node: 1
5. Returns [1, setCount]
6. JSX renders with count = 1 → user sees "Count: 1"
KEY INSIGHT: initialValue is ONLY used on the very first render!
On every subsequent render, useState returns the STORED value.
3. useState vs Regular Variables
Feature useState Regular variable (let)
──────────────────────────────────────────────────────────────────
Persists across ✅ YES ❌ NO (resets each render)
renders?
Triggers re-render ✅ YES (when changed) ❌ NO (React ignores it)
when changed?
Read in JSX ✅ YES ✅ YES (but stale!)
Mutate directly ❌ NO (use setter) ✅ YES (but pointless)
// Demonstration:
function Demo() {
let regularVar = 0; // Resets to 0 on EVERY render!
const [stateVar, setStateVar] = useState(0); // Persists!
console.log("Rendering! regular:", regularVar, "state:", stateVar);
// Click "+regular": regularVar goes to 1, but component re-renders...
// ...and regularVar is RESET back to 0! 😱
// Click "+state": stateVar goes to 1, re-renders, stateVar is STILL 1 ✅
return (
<div>
<p>Regular: {regularVar}</p> {/* Always 0 */}
<button onClick={() => regularVar++}>+regular</button>
<p>State: {stateVar}</p> {/* Correct! */}
<button onClick={() => setStateVar(stateVar + 1)}>+state</button>
</div>
);
}
4. Functional Update Form — prev => prev + 1
THE PROBLEM with setCount(count + 1):
======================================
// Imagine clicking the button 3 times very fast:
function Counter() {
const [count, setCount] = useState(0);
function handleTripleIncrement() {
setCount(count + 1); // count is 0, so queues: set to 1
setCount(count + 1); // count is STILL 0! queues: set to 1 again!
setCount(count + 1); // count is STILL 0! queues: set to 1 again!
// Result: count becomes 1, not 3! ❌
}
}
WHY? 'count' is the value captured in this render's closure.
All three setCount calls see the SAME count value (0).
React batches them and applies the last one: set to 1.
THE SOLUTION — Functional update form:
========================================
function handleTripleIncrement() {
setCount(prev => prev + 1); // prev = 0, queues: set to 1
setCount(prev => prev + 1); // prev = 1 (from previous), queues: set to 2
setCount(prev => prev + 1); // prev = 2, queues: set to 3
// Result: count becomes 3! ✅
}
// Functional update form:
setCount(prev => prev + 1)
// 'prev' is always the LATEST state at time of execution
// React chains functional updates sequentially!
WHEN TO USE functional update:
// ✅ Use when next state depends on previous state
setCount(prev => prev + 1)
setItems(prev => [...prev, newItem])
setUser(prev => ({ ...prev, name: "New Name" }))
// When next state doesn't depend on previous:
setCount(5) // Just set to 5
setName("Alice") // Just set to "Alice"
// → functional form not needed
5. Wiring Up Live Search
// Full working search with useState:
import { useState } from "react";
const Body = () => {
// State for the search input text
const [searchText, setSearchText] = useState("");
// State for the displayed list (starts as full list)
const [filteredRestaurants, setFilteredRestaurants] = useState(resList);
const handleSearch = () => {
const filtered = resList.filter(res =>
res.info.name.toLowerCase().includes(searchText.toLowerCase())
);
setFilteredRestaurants(filtered);
};
const handleTopRated = () => {
const topRated = resList.filter(res => res.info.avgRating >= 4.5);
setFilteredRestaurants(topRated);
};
return (
<div className="body">
{/* Search bar — CONTROLLED input */}
<div className="search">
<input
type="text"
className="search-box"
value={searchText}
onChange={e => setSearchText(e.target.value)}
placeholder="Search for restaurants..."
/>
<button onClick={handleSearch}>Search</button>
</div>
{/* Top Rated Filter */}
<button
className="filter-btn"
onClick={handleTopRated}
>
Top Rated Restaurants
</button>
{/* Restaurant Grid */}
<div className="res-container">
{filteredRestaurants.map(restaurant => (
<RestaurantCard
key={restaurant.info.id}
resData={restaurant}
/>
))}
</div>
</div>
);
};
6. useState with Objects and Arrays
// STATE WITH OBJECTS:
const [user, setUser] = useState({
name: "Akshay",
age: 25,
email: "akshay@example.com"
});
// ❌ WRONG — mutating state directly (NEVER do this!):
user.name = "Namaste"; // React doesn't see this change!
setUser(user); // Same reference → React thinks nothing changed → no re-render!
// ✅ CORRECT — always create a NEW object:
setUser({ ...user, name: "Namaste" }); // Spread old, override changed field
// Or with functional update:
setUser(prev => ({ ...prev, name: "Namaste" }));
// STATE WITH ARRAYS:
const [items, setItems] = useState(["Apple", "Banana"]);
// ❌ WRONG — mutating array directly:
items.push("Cherry"); // Mutates the same array reference → no re-render!
setItems(items); // Same reference → React skips re-render!
// ✅ CORRECT — always return a NEW array:
setItems([...items, "Cherry"]); // Add to end
setItems(prev => [newItem, ...prev]); // Add to start
setItems(items.filter(item => item !== "Banana")); // Remove
setItems(items.map(item => item === "Apple" ? "Mango" : item)); // Update
GOLDEN RULE: NEVER MUTATE STATE DIRECTLY!
==========================================
React uses reference equality to detect changes.
If you mutate and return the same reference → React sees no change → no re-render!
Always return a NEW object/array with your changes!
7. The Stale Closure Problem
// The stale closure problem happens with async operations:
function Timer() {
const [count, setCount] = useState(0);
function handleClick() {
// This setTimeout "closes over" the current count value
setTimeout(() => {
// ⚠️ STALE CLOSURE: count here is the value at the time
// handleClick was called, NOT the current count 3 seconds later!
console.log("Count is:", count); // Always logs the value from when click happened!
setCount(count + 1); // Might not add to latest count!
}, 3000);
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>+1 (after 3s)</button>
</div>
);
}
// Steps that cause the bug:
// 1. count = 0. User clicks → setTimeout(fn, 3000) captures count=0
// 2. User clicks again → another setTimeout captures count=0 again
// 3. 3 seconds pass. Both timeouts fire.
// 4. First: setCount(0 + 1) = 1
// 5. Second: setCount(0 + 1) = 1 ← wrong! Should be 2!
// FIX — use functional update form:
setTimeout(() => {
setCount(prev => prev + 1); // prev is always the LATEST value ✅
}, 3000);
// Functional updates receive the ACTUAL latest state
// regardless of what the closure captured!
Interview Questions
Q: What is useState?
"useState is a React Hook that lets functional components have state. It takes an initial value and returns an array of two things: the current state value and a setter function. When you call the setter, React re-renders the component with the new value. The initial value is only used on the first render — subsequent renders use the stored value from React's fiber node."
Q: Why can't we use regular variables instead of useState?
"Two reasons. First, regular variables reset to their initial value on every render — they have no persistence. Second, changing a regular variable doesn't tell React anything changed, so React never re-renders and the screen never updates. useState solves both: the value persists across renders in the Fiber node, and calling the setter function signals React to re-render."
Q: When should you use the functional update form: setState(prev => ...)?
"You should use the functional update form whenever the new state depends on the previous state. The callback receives the guaranteed latest state value, avoiding the stale closure problem. Classic cases: incrementing counters (prev => prev + 1), toggling booleans (prev => !prev), adding to arrays (prev => [...prev, newItem]), and any async operation like setTimeout or fetch where the closure might capture a stale value."
Q: Why must you never mutate state directly in React?
"React uses reference equality to detect state changes. If you mutate an object or array in place (like items.push()), the reference stays the same — React compares old and new references, sees they're identical, and skips the re-render entirely. You must always create a new object or array so React sees a different reference and triggers a re-render. For objects: spread operator. For arrays: spread, filter, map."
Key Points to Remember
| Concept | Key Takeaway |
|---|---|
| State | A variable React watches. Change it → React re-renders. Persists across renders. |
| useState syntax | const [value, setValue] = useState(initial). Initial value used only once. |
| Setter triggers re-render | setValue(newVal) tells React to re-render the component. |
| Functional update | setState(prev => ...) — use when new state depends on previous. Prevents stale closure. |
| Never mutate state | Always return new object/array. Mutation breaks React's change detection. |
| Stale closure | Async callbacks capture old state values. Fix with functional updates. |
| Controlled input | value={state} + onChange={setter}. State always reflects what's in the input. |
What's Next?
In Chapter 06, we learn useEffect and make real API calls to fetch restaurant data from the internet — no more hardcoded arrays!
Keep coding, keep learning! See you in the next one!
Post a Comment