React hooks look simple until they surprise you in production. useState batching in React 18 changes when re-renders fire. useEffect with wrong dependencies causes infinite loops or stale closures. useCallback and useMemo are misused more than any other hooks. This guide covers every hook in depth — the rules, the traps, and the patterns that work reliably.
⚡ TL;DR: useState for local UI state. useEffect for side effects with cleanup. useCallback for stable function references (only when passed to memoized children). useMemo for expensive calculations. useRef for mutable values that don’t trigger re-renders. Custom hooks to share stateful logic.
useState — batching and functional updates
import { useState } from 'react';
// React 18: automatic batching — multiple setState = ONE re-render
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
const handleClick = () => {
setCount(c => c + 1); // Functional update: safe when next state depends on previous
setName('updated');
// React 18: ONE re-render (batched), not two
};
}
// Functional update pattern — avoids stale closure bugs
const [count, setCount] = useState(0);
// WRONG: setCount(count + 1) — count may be stale in closures
// RIGHT: setCount(c => c + 1) — c is always current
// Object state: always spread to preserve other fields
const [form, setForm] = useState({ name: '', email: '' });
const handleName = (e) => setForm(prev => ({ ...prev, name: e.target.value }));
// Lazy initialization: for expensive initial computation
const [data, setData] = useState(() => expensiveCompute()); // Called onceuseEffect — dependencies, cleanup, and timing
import { useEffect, useState } from 'react';
// The dependency array:
// [] — run once on mount (and cleanup on unmount)
// [dep1, dep2] — run when dep1 or dep2 changes
// (omitted) — run after every render (usually wrong)
// Cleanup — ALWAYS clean up subscriptions, timers, event listeners
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setCount(c => c + 1), 1000);
return () => clearInterval(id); // Cleanup: runs before next effect OR unmount
}, []); // Empty deps: set up once, clean up on unmount
}
// Data fetching with cleanup (prevent setState on unmounted component)
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let cancelled = false;
fetchUser(userId).then(data => {
if (!cancelled) setUser(data); // Only set if still mounted
});
return () => { cancelled = true; };
}, [userId]); // Re-run when userId changes
}
// Stale closure bug — classic mistake
function Bad() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count); // STALE: count is always 0 (closed over initial value)
}, 1000);
return () => clearInterval(id);
}, []); // Missing count in deps!
}
// Fix: add count to deps, or use functional updateuseCallback and useMemo — when they actually help
// useCallback: stable function reference
// ONLY helps when the function is passed to a React.memo component
function Parent() {
const [count, setCount] = useState(0);
// Without useCallback: new function reference every render
// MemoizedChild would re-render even though it shouldn't
const handleDelete = useCallback((id) => {
deleteItem(id);
}, []); // No deps: function never changes
return ;
}
// Rule: useCallback is ONLY useful when the function is a prop to a memoized child
// useMemo: cache expensive computation
function ProductList({ products, query, sort }) {
const filtered = useMemo(() => {
return products
.filter(p => p.name.includes(query))
.sort((a, b) => a[sort] > b[sort] ? 1 : -1);
}, [products, query, sort]);
// Only recalculates when products, query, or sort changes
// NOT on every render
}
// Rule: useMemo for computation that takes >1ms or returns object/array passed to memo childuseRef — mutable values without re-renders
import { useRef, useEffect } from 'react';
// DOM reference
function Input() {
const inputRef = useRef(null);
useEffect(() => { inputRef.current.focus(); }, []);
return ;
}
// Mutable value that does NOT trigger re-render
function StopWatch() {
const [time, setTime] = useState(0);
const intervalRef = useRef(null); // Store interval ID without re-render
const start = () => {
intervalRef.current = setInterval(() => setTime(t => t + 1), 100);
};
const stop = () => clearInterval(intervalRef.current);
}
// Previous value pattern
function usePrevious(value) {
const ref = useRef();
useEffect(() => { ref.current = value; }); // Runs after render
return ref.current; // Returns previous render's value
}
// useState vs useRef:
// useState: triggers re-render when changed
// useRef: no re-render, value persists across renders, synchronous accessCustom hooks — share stateful logic
// Extract stateful logic into reusable hooks
function useLocalStorage(key, defaultValue) {
const [value, setValue] = useState(() => {
try { return JSON.parse(localStorage.getItem(key)) ?? defaultValue; }
catch { return defaultValue; }
});
const set = useCallback((newValue) => {
setValue(newValue);
localStorage.setItem(key, JSON.stringify(newValue));
}, [key]);
return [value, set];
}
// Usage:
const [theme, setTheme] = useLocalStorage('theme', 'light');
// useFetch — data fetching hook
function useFetch(url) {
const [state, setState] = useState({ data: null, loading: true, error: null });
useEffect(() => {
setState(s => ({ ...s, loading: true }));
fetch(url)
.then(r => r.json())
.then(data => setState({ data, loading: false, error: null }))
.catch(error => setState({ data: null, loading: false, error }));
}, [url]);
return state;
}React Hooks cheat sheet
- ✅ Always use functional updates:
setState(prev => prev + 1) - ✅ Always return cleanup from useEffect for subscriptions and timers
- ✅ ESLint
exhaustive-depsrule — never suppress it without understanding why - ✅ useCallback only when function is prop to memoized child
- ✅ useMemo only when computation is measurably expensive
- ✅ useRef for DOM refs and mutable values that shouldn’t trigger re-renders
- ❌ Never call hooks inside conditions or loops
- ❌ Never omit dependencies from useEffect — stale closures are silent bugs
React hooks performance connects to the React performance optimization guide — hooks are the mechanism, performance optimization is the goal. For state management at scale, the JavaScript Proxy guide shows how state management libraries implement reactivity. External reference: React Hooks API reference.
Free Weekly Newsletter
🚀 Don’t Miss the Next Cheat Code
Related: React State Management: useState vs useReducer vs Context vs Zustand · Day 12: ReAct vs Plan-Then-Act vs Compiler Agents · Node.js Async Hooks: The Tracing API That Powers Every APM Tool
Related: Lambda Alias Traffic Shifting: Automated Rollback Hooks That Fire · DynamoDB Streams: Session Expiry Hooks for Multi-Turn Coding Agents · Claude Code Hooks: Gate Risky Shell Commands Before CI Runs
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
