Core Concepts10 min read · Updated 2025-06-01

React Hooks Explained: useState, useEffect, useRef & the Rules

A complete guide to React Hooks — why they exist, the rules that govern them, and how useState, useEffect, and useRef work under the hood.

💡 Practice these concepts interactively with AI feedback

Start Practicing →

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 confusionthis 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).

📚 Practice These Topics
UseState
4–8 questions
UseCallback
4–8 questions
Custom hook
4–8 questions
UseReducer
4–8 questions

Put This Into Practice

Reading articles is passive. JSPrep Pro makes you actively recall, predict output, and get AI feedback.

Start Free →Browse All Questions

Related Articles

Deep Dive
We Built a RAG-Powered AI Question Engine Into a JavaScript Interview Platform — Here's Exactly How It Works
12 min read
Build Systems
Monorepo with Turborepo vs Nx: The Complete Comparison (2025)
9 min read
Core Concepts
map() vs forEach() in JavaScript: Which One to Use and Why It Matters
7 min read