By Rahul — Google Frontend Engineer
What Are Hooks?
Hooks let you use state, lifecycle, and other React features in function components. Introduced in React 16.8, they replaced the need for class components in most cases.
// Before hooks: class component
class Counter extends React.Component {
state = { count: 0 };
componentDidMount() { document.title = this.state.count; }
componentDidUpdate() { document.title = this.state.count; }
render() {
return <button onClick={() => this.setState({ count: this.state.count + 1 })}>
{this.state.count}
</button>;
}
}
// After hooks: function component
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => { document.title = count; }, [count]);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}Core Hooks
useState() // State management
useEffect() // Side effects (API calls, subscriptions, DOM manipulation)
useContext() // Consume context without wrapper
useReducer() // Complex state logic
useRef() // Mutable value that persists across renders
useMemo() // Memoize expensive calculations
useCallback() // Memoize functions
useId() // Generate unique IDs for accessibilityPros
- Simpler code: No more
thisbinding, constructor, or render method - Logic reuse: Custom hooks let you extract and share stateful logic without HOCs or render props
- Colocation: Related logic stays together instead of being split across lifecycle methods
- Composable: Hooks compose naturally — call one hook inside another
- Smaller bundle: Function components are smaller than classes after minification
Cons
- Stale closures: The most common bug. Callbacks capture old values
- Dependency arrays: Getting them wrong causes bugs (missing deps) or performance issues (too many deps)
- useEffect complexity: Mixing multiple concerns in one useEffect is tempting but bad
- Rules of Hooks: Cannot call hooks conditionally or in loops — sometimes feels limiting
The Stale Closure Problem
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // BUG: count is always 0 (stale closure)
}, 1000);
return () => clearInterval(id);
}, []); // Empty deps = closure captures initial count
// FIX: use functional update
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1); // Gets current value
}, 1000);
return () => clearInterval(id);
}, []);
}Custom Hooks
// Extract reusable logic into custom hooks
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Usage
const [theme, setTheme] = useLocalStorage('theme', 'dark');Summary
Hooks simplify React development by enabling state and effects in function components. They enable powerful code reuse through custom hooks. Watch out for stale closures and dependency array mistakes. Use functional updates and the exhaustive-deps ESLint rule.