
ReactJS
Beyond basic useState usage lies a deep understanding of Hook Rules, closure behavior, and functional updates. These concepts are essential for writing correct, predictable state code at scale.
React Hooks have strict rules that must be followed. Violating them causes bugs that are hard to track. These rules exist because Hooks rely on call order to maintain state associations.
Hooks depend on call order to track state. If you call hooks conditionally or in different orders between renders, React can't match state values to their updater functions, leading to silent state corruption.
import { useState } from 'react';
// β WRONG: Conditional hook calls
function BadCounter({ showCount }: { showCount: boolean }) {
if (showCount) {
const [count, setCount] = useState(0); // Call order changes!
}
// This breaks React's state tracking
return null;
}
// β WRONG: Hooks in loops
function BadList({ items }: { items: string[] }) {
for (let i = 0; i < items.length; i++) {
const [state, setState] = useState(i); // Call order changes with items!
}
return null;
}
// β
CORRECT: Hooks at top level
function GoodCounter({ showCount }: { showCount: boolean }) {
const [count, setCount] = useState(0);
const [isVisible, setIsVisible] = useState(showCount);
// Logic based on state, not hook calls
if (showCount) {
return <p>Count: {count}</p>;
}
return null;
}
// β
CORRECT: Extract to custom hook instead of looping
function useListItem(initialValue: string) {
const [value, setValue] = useState(initialValue);
return [value, setValue] as const;
}
function GoodList({ items }: { items: string[] }) {
// Each item's state is in a separate component
return (
<div>
{items.map((item) => (
<ListItem key={item} initial={item} />
))}
</div>
);
}
function ListItem({ initial }: { initial: string }) {
const [value, setValue] = useListItem(initial);
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}<details>
<summary>π More Examples</summary>
// Example: Common mistakes with hook rules
// β WRONG: Inside callbacks or event handlers
function BadClick() {
const handleClick = () => {
const [count, setCount] = useState(0); // Wrong!
};
return <button onClick={handleClick}>Click</button>;
}
// β
CORRECT: Hooks only at component level
function GoodClick() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1); // Use state from component level
};
return <button onClick={handleClick}>Count: {count}</button>;
}
// β WRONG: Hooks in try-catch or async logic
async function BadAsync() {
try {
const [data, setData] = useState(null); // Wrong!
} catch (e) {
// Error handling
}
}
// β
CORRECT: Move async logic to useEffect
function GoodAsync() {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const result = await fetch('/api/data');
setData(await result.json());
} catch (e) {
console.error(e);
}
};
fetchData();
}, []);
return <p>{data ? 'Loaded' : 'Loading'}</p>;
}</details>
State exists in closures. Each render captures its own version of state. Understanding this prevents common bugs with stale state and outdated references.
When a component renders, it creates a closure over current state values. Event handlers, callbacks, and effects inside that render capture those values. If you use the same function later, it still has the old valuesβthis is closure behavior, not a React bug.
import { useState, useRef } from 'react';
function ClosureDemonstration() {
const [count, setCount] = useState(0);
const countRef = useRef(0);
// This button uses current count at click time (closure)
const handleClickImmediate = () => {
alert(`Count is: ${count}`); // Uses closure from render
};
// This function delays 3 seconds, count has changed by then
const handleClickDelayed = () => {
setTimeout(() => {
alert(`Count is: ${count}`); // Still has old value from closure!
}, 3000);
};
// This function uses ref, which is mutable
const handleClickRef = () => {
countRef.current = count; // Update ref
setTimeout(() => {
alert(`Ref value: ${countRef.current}`); // Gets latest value
}, 3000);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={handleClickImmediate}>Alert Now</button>
<button onClick={handleClickDelayed}>Alert in 3s</button>
<button onClick={handleClickRef}>Alert Ref in 3s</button>
</div>
);
}<details>
<summary>π More Examples</summary>
// Example: Closure problems in event handlers
function FormWithClosure() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
// β BAD: Closure captures stale values
const handleSubmitBad = async () => {
// If user changes email after clicking, this still uses old value
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }), // Stale closure!
});
};
// β
GOOD: Functional update or useCallback with dependencies
const handleSubmitGood = async (e: React.FormEvent) => {
e.preventDefault();
// Get current values from the form
const formData = new FormData(e.currentTarget);
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({
email: formData.get('email'),
password: formData.get('password'),
}),
});
};
return (
<form onSubmit={handleSubmitGood}>
<input
name="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
name="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Submit</button>
</form>
);
}</details>
Instead of passing new state directly, pass a function that receives previous state. This prevents closure bugs and enables proper batching in React 18.
Functional updates isolate each state update, making them independent. React can batch these updates safely, even when they depend on each other.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
// β PROBLEMATIC: Direct updates can cause issues
const incrementBad = () => {
setCount(count + 1); // Uses closure value
setCount(count + 1); // Both use same closure value!
// Result: +1, not +2
};
// β
CORRECT: Functional updates
const incrementGood = () => {
setCount((prevCount) => prevCount + 1);
setCount((prevCount) => prevCount + 1);
// Result: +2 (each sees previous update)
};
// Complex example: updating dependent state
const [items, setItems] = useState<string[]>([]);
const [totalLength, setTotalLength] = useState(0);
// β BAD: Total gets stale value
const addItemBad = (item: string) => {
setItems([...items, item]);
setTotalLength(items.length + 1); // Uses closure value!
};
// β
GOOD: Derive from items or use functional update
const addItemGood = (item: string) => {
setItems((prevItems) => {
const newItems = [...prevItems, item];
setTotalLength(newItems.length); // Correct length!
return newItems;
});
};
return (
<div>
<p>Count: {count}</p>
<button onClick={incrementGood}>Increment</button>
<p>Items: {items.length}</p>
<button onClick={() => addItemGood('new')}>Add Item</button>
</div>
);
}<details>
<summary>π More Examples</summary>
// Example: Complex state with functional updates
interface AppState {
notifications: Array<{ id: string; message: string }>;
unreadCount: number;
lastUpdated: Date;
}
function NotificationCenter() {
const [state, setState] = useState<AppState>({
notifications: [],
unreadCount: 0,
lastUpdated: new Date(),
});
// Add and update multiple values atomically
const addNotification = (message: string) => {
setState((prevState) => ({
...prevState,
notifications: [
...prevState.notifications,
{ id: Date.now().toString(), message },
],
unreadCount: prevState.unreadCount + 1,
lastUpdated: new Date(),
}));
};
// Conditional functional update
const dismissNotification = (id: string) => {
setState((prevState) => {
const dismissed = prevState.notifications.find((n) => n.id === id);
return {
...prevState,
notifications: prevState.notifications.filter((n) => n.id !== id),
unreadCount: Math.max(0, prevState.unreadCount - 1),
lastUpdated: new Date(),
};
});
};
return (
<div>
<p>Notifications: {state.unreadCount}</p>
<button onClick={() => addNotification('Hello!')}>Add</button>
{state.notifications.map((n) => (
<div key={n.id}>
{n.message}
<button onClick={() => dismissNotification(n.id)}>Dismiss</button>
</div>
))}
</div>
);
}</details>
Expensive initialization should happen only once. Pass a function to useState to defer calculation until the first render.
If your initial state requires expensive computation (fetching, calculating, etc.), use a function. React calls this function only on the first render, not on every render.
import { useState, useMemo } from 'react';
// β BAD: Heavy computation runs every render
function BadInitialization() {
const expensiveValue = computeExpensiveValue(); // Always runs!
const [value, setValue] = useState(expensiveValue);
return <p>{value}</p>;
}
// β
GOOD: Lazy initialization
function GoodInitialization() {
const [value, setValue] = useState(() => computeExpensiveValue());
// Function only called once on mount
return <p>{value}</p>;
}
// Complex initialization example
interface CachedData {
items: string[];
timestamp: number;
hash: string;
}
function DataComponent() {
const [cache, setCache] = useState<CachedData>(() => {
// Load from localStorage, compute hash, etc.
const stored = localStorage.getItem('cache');
if (stored) {
try {
return JSON.parse(stored);
} catch (e) {
console.error('Invalid cache');
}
}
// Compute default
return {
items: [],
timestamp: Date.now(),
hash: '',
};
});
return (
<div>
<p>Cached items: {cache.items.length}</p>
</div>
);
}βββββββββββββββββββββββββββ¬βββββββββββββββββββ¬βββββββββββββββββββ
β Pattern β Best For β Pitfalls β
βββββββββββββββββββββββββββΌβββββββββββββββββββΌβββββββββββββββββββ€
β Direct update β Independent vars β Stale closures β
β Functional update β Dependent values β More verbose β
β Lazy initialization β Heavy compute β Forgotten func β
β Reducer pattern β Complex state β More boilerplate β
βββββββββββββββββββββββββββ΄βββββββββββββββββββ΄βββββββββββββββββββReady to practice? Challenges | Next: State Updates and Batching
Resources
Ojasa Mirai
Master AI-powered development skills through structured learning, real projects, and verified credentials. Whether you're upskilling your team or launching your career, we deliver the skills companies actually need.
Learn Deep β’ Build Real β’ Verify Skills β’ Launch Forward