React Hooks Explained
Hooks are the most tested React topic in frontend interviews. Every React interview includes at least one hook question.
Why Hooks Were Introduced
Before React 16.8, stateful logic required class components. The problems:
1. No way to reuse stateful logic — HOCs and render props added component nesting just to share state 2. Complex components — lifecycle methods forced unrelated logic to coexist (componentDidMount doing three different things) 3. Class confusion — this binding bugs, no equivalent in non-JS languages
Hooks solve all three: stateful logic in functions, logic grouped by concern, no classes required.
The Two Rules of Hooks
These are non-negotiable — React's hook ordering mechanism depends on them:
Rule 1: Only call hooks at the top level
// ❌ Hook inside a condition — breaks ordering if (isLoggedIn) { const [user, setUser] = useState(null) }
// ✅ Always at top level const [user, setUser] = useState(null) if (!isLoggedIn) return <Login />
Rule 2: Only call hooks from React function components or custom hooks Not from regular JavaScript functions, class methods, or event handlers.
Why these rules exist: React tracks hooks by their call order, not by name. If the order changes between renders (e.g., a conditional hook), React maps state to the wrong hook.
useState: Reactive State
useState(initialValue) returns [currentValue, setter]. Calling the setter schedules a re-render with the new value.
const [count, setCount] = useState(0)
// Direct update setCount(5)
// Functional update — always receives latest state setCount(prev => prev + 1) // use this when new state depends on old
Critical: never mutate state directly
// ❌ Mutation — same reference, no re-render state.name = 'Alice' setState(state)
// ✅ New reference — triggers re-render setState({ ...state, name: 'Alice' })
Lazy initializer: Pass a function when initial value is expensive to compute:
// ❌ Runs on EVERY render const [data, setData] = useState(parseHeavyJSON(rawData))
// ✅ Runs only ONCE (on mount) const [data, setData] = useState(() => parseHeavyJSON(rawData))
React 18 batching: Multiple setState calls inside a single event handler are batched into one re-render. In React 18, this also applies inside setTimeout and Promise callbacks.
useEffect: Synchronizing with the Outside World
useEffect lets you "step outside" React to interact with the DOM, APIs, timers, or subscriptions — things that aren't part of React's pure render cycle.
useEffect(() => {
// side effect runs here (after render)
document.title = 'Page: ' + count
return () => { // cleanup runs before NEXT effect fires, and on unmount document.title = 'App' } }, [count]) // only re-run when count changes
The three dependency array forms:
| Form | When it runs | |------|-------------| | No array | After every render | | [] | Once, after mount | | [a, b] | After mount + whenever a or b changes |
The most common useEffect bug — stale closure:
// ❌ count is captured at 0, never updates useEffect(() => { const id = setInterval(() => console.log(count), 1000) return () => clearInterval(id) }, []) // missing count in deps!
// ✅ Include all values used inside the effect useEffect(() => { const id = setInterval(() => console.log(count), 1000) return () => clearInterval(id) }, [count])
useRef: Mutable Values That Don't Cause Re-renders
useRef(initialValue) returns { current: initialValue }. Mutating .current doesn't trigger a re-render.
Use 1: DOM references
const inputRef = useRef(null) // attach: <input ref={inputRef} /> function focusInput() { inputRef.current.focus() // direct DOM access }
Use 2: Storing values across renders without re-rendering
const timerRef = useRef(null) function start() { timerRef.current = setTimeout(() => ..., 1000) } function stop() { clearTimeout(timerRef.current) }
The key distinction: useState for values that affect the UI. useRef for values that need to persist but don't need to be rendered.
Custom Hooks: Extracting Reusable Logic
Any function starting with use that calls hooks is a custom hook. They let you share stateful logic between components.
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth)
useEffect(() => { const handler = () => setWidth(window.innerWidth) window.addEventListener('resize', handler) return () => window.removeEventListener('resize', handler) }, [])
return width }
// Used in any component — no hierarchy changes needed function Header() { const width = useWindowWidth() return <nav>{width > 768 ? <DesktopNav /> : <MobileNav />}</nav> }
Practice hooks questions at [JSPrep Pro](/auth).