DeepFrontend
Practice ArenaBlogSign in
Go Pro
DeepFrontend

Interview-grade learning paths and hands-on practice for mid-to-senior engineers. Master internals, patterns, and system architecture with runnable code.

All systems operational

Core Courses

  • -JavaScript Internals
  • -React Reconciliation
  • -TypeScript rigor
  • -Next.js Caching
  • -Node.js Event Loop
  • -System Design
  • -Web Security
  • -CSS & Page Layouts

Explore

  • Learning Paths
  • Practice Arena
  • Pricing Options
  • HTML Sitemap

Resources

  • Privacy Policy
  • Terms of Service
  • Email Support

© 2026 DeepFrontend. All rights reserved.

Expert learning environments for web engineering teams.

Home/React/Hooks Overview
intermediate25 min read·Updated Jun 2025

Hooks Overview

A hook is just a function whose return value depends on a hidden, ordered list attached to the component instance — read by CALL ORDER, not by name. Every 'rule' of hooks falls directly out of that one mechanical fact.

hooksrules-of-hooksuseStateuseEffectfundamentals

Knowledge Check

4 questions · pass at 70%

0/4
easymcq

1. How does React know which stored value a given useState() call should return on re-render?


Interview Questions

2 questions

Cheatsheet

Download-ready reference
Cheatsheet
Hooks are stored in an ORDERED LIST per component instance —
  matched by CALL ORDER (slot position), never by name or identity.

Rules of Hooks:
  1. Only call hooks at the TOP LEVEL — never in a condition, loop,
     or nested function.
  2. Only call hooks from a React function component or another
     custom hook — never from a regular helper function.

WHY: skipping a hook call on some renders shifts every hook call
  AFTER it to the wrong slot → corrupted state or a thrown error
  ("Rendered fewer hooks than expected" / order of Hooks changed).

Fix for "I need this conditionally": push the condition INSIDE the
  hook's callback/body, never around the hook CALL itself.
    ✅ useEffect(() => { if (!flag) return; ... }, [flag])
    ❌ if (flag) { useEffect(() => { ... }, []) }

Custom hooks are just regular functions that call other hooks —
  the `use` prefix is a CONVENTION for tooling, not a language rule.

Related topics

effectsRendering LifecycleProCustom HooksPro

On this page

25m
Knowledge CheckInterview QuestionsCheatsheet

Concept#

The beginner framing: hooks are special functions — useState, useEffect, useContext, and others — that let a function component do things only class components used to be able to do: remember values across renders, run code after rendering, and tap into shared data from further up the tree.

The precise mental model: for each component instance, React keeps a hidden, ordered list of "hook slots" — one entry per hook call, in the exact order those hooks were called during the first render. On every subsequent render, React walks that same list in call order and hands each hook call the state that lives in its slot. There's no lookup by variable name or hook type involved — slot 0 is whatever was called first, slot 1 is whatever was called second, and so on, forever, for the lifetime of that component instance.

function Profile() {
  const [name, setName] = useState("Ada");   // slot 0
  const [age, setAge] = useState(36);        // slot 1
  useEffect(() => { document.title = name; }, [name]); // slot 2
  // ...
}

This single fact — slots are matched by call order, not identity — is the entire reason the Rules of Hooks exist.

The Rules of Hooks#

  1. Only call hooks at the top level. Never inside a condition, loop, or nested function.
  2. Only call hooks from React function components or from other custom hooks. Never from a regular helper function or an event handler.

These aren't stylistic guidelines — they're the only way to guarantee the same hooks run in the same order on every render, which is the sole thing that makes the slot list work at all.

Hooks are read by call order — a conditional hook shifts every slot after it
step 1 / 4
function Profile({ isEditing }) {
const [name, setName] = useState("Ada"); // slot 0
const [draft, setDraft] = useState(name); // slot 1
useEffect(() => { ... }, [name]); // slot 2
return /* ... */;
}
Render 1 · isEditing = true
#0useState"Ada"
#1useState"Ada"
#2useEffectdeps: [name]

Hooks are stored in an ordered list — a "slot" array — per component instance, indexed purely by the order they're CALLED in, not by name or variable. This safe version calls all three hooks unconditionally, every render.

A quick tour of the built-ins#

HookWhat it gives you
useStateA value that persists across renders, plus a setter that schedules a re-render (see State)
useEffectA way to synchronize with something outside React after commit (see Effects)
useContextReads the nearest matching Provider's value without prop-drilling
useRef

Each of these is covered in depth in its own topic — this page is about the one mechanical rule that governs all of them.


Try It#

Predict what happens before checking the solution.

function Settings({ showAdvanced }) {
  const [name, setName] = useState("Ada");
 
  if (showAdvanced) {
    const [advancedMode, setAdvancedMode] = useState(false); // ⚠️
  }
 
  useEffect(() => {
    console.log



What happens if showAdvanced is true on the first render, then becomes false on the second?

Solution

React throws an error — something like "Rendered fewer hooks than expected" or "React has detected a change in the order of Hooks." On the first render, showAdvanced is true, so all three hooks run: slot 0 (name), slot 1 (advancedMode), slot 2 (useEffect). On the second render, showAdvanced is false — the if block is skipped, so only two hook calls happen: slot 0 (name), and then useEffect lands in slot 1, the slot React has on record as a useState call from last render. React can't reconcile a useState slot with an effect call, so it throws rather than silently corrupting state. Walk through the exact mismatch step-by-step in the visualizer above.


Implement It Yourself#

Build a minimal version of the hook slot mechanism — a tiny useState clone that shows exactly why call order matters:

let hookSlots = [];
let slotIndex = 0;
 
function resetHooksForRender() {
  slotIndex = 0; // rewind to the start of the list before each render
}
 
function useState(initialValue) {
  const currentIndex = slotIndex; // capture THIS call's slot, by position
 
  if (hookSlots[currentIndex] === undefined) {
    hookSlots[currentIndex] 

















Notice useState never receives any identifier for which piece of state it's returning — only slotIndex, which is purely a function of call order. This is precisely why skipping a hook call on some renders (but not others) silently shifts every subsequent slot.


Under the Hood#

The hook slot list is, at its core, an ordered list very similar to the arrays and linked lists covered in Data Structures — React's actual Fiber implementation stores hooks as a linked list attached to the fiber node, walked one node at a time on every render. And the reason a hook can "remember" a value across calls at all is the same mechanism as Closures: the slot list itself is kept alive across renders (much like a makeCounter()-style closure keeps its enclosing scope alive), even though each render is, mechanically, a brand-new call to the component function (see Execution Context).


Common Mistakes#

1. Calling a hook inside a condition, loop, or nested function#

if (isLoggedIn) {
  const [user, setUser] = useState(null); // ❌ conditional
}
 
items.forEach(() => {
  useState(0); // ❌ inside a loop
});

Covered above in depth — any hook call that doesn't happen unconditionally, in the same order, every render, risks a slot mismatch. Fix: call the hook unconditionally at the top level, and put the conditional logic inside the hook's body or around how its value is used, not around the call itself.

2. Calling a hook from a plain helper function#

function getUserDisplayName() {
  const [name] = useState("Ada"); // ❌ not a component, not a custom hook
  return name;
}

Hooks rely on React tracking "which component/hook is currently rendering" — a plain function called from anywhere (including outside render) has no such tracking. Only call hooks directly inside a function component's body or another custom hook (see Custom Hooks).

3. Calling a hook after an early return#

function Profile({ user }) {
  if (!user) return null; // returns HERE...
  const [tab, setTab] = useState("overview"); // ❌ ...so this hook is sometimes skipped
  return /* ... */;
}

This is the conditional-hook mistake in disguise — the useState call only happens on renders where user is truthy, which is exactly the kind of call-order inconsistency the Rules of Hooks forbid.


Best Practices#

  • Install eslint-plugin-react-hooks (bundled with most React toolchains) — it statically catches conditional/looped hook calls and missing effect dependencies before they ever reach runtime.
  • Name every custom hook starting with use — this isn't just convention, it's what lets the lint rule (and other engineers) recognize it's subject to the Rules of Hooks in the first place.
  • Push conditional logic inside the hook, not around the call — useState(isAdvanced ? {} : null) is fine; if (isAdvanced) { useState() } is not.
  • Keep the number and order of hook calls independent of props/state — if you need a variable number of stateful values, store them in a single useState/useReducer holding an array or object, rather than calling a hook a variable number of times.

Performance Tips#

  • The slot-list mechanism itself is cheap — the cost of hooks comes from what's inside them (an expensive computation in useMemo, an expensive effect), not from the bookkeeping of the list.
  • Because slot identity depends only on call order, adding or removing a hook call conditionally is a correctness bug long before it's a performance concern — always resolve a "changed order of Hooks" warning immediately rather than treating it as noise.

A mutable box that persists across renders and does NOT trigger a re-render on write (see Refs & the DOM)
useReducerLike useState, but state transitions go through a reducer function — useful once update logic gets complex
useMemo / useCallbackCache a computed value / function reference across renders, given the same dependencies
(
"settings changed"
);
}, [name]);
return /* ... */;
}
=
initialValue;
// first render: initialize the slot
}
function setState(newValue) {
hookSlots[currentIndex] = newValue;
}
slotIndex++; // advance to the next slot for the NEXT hook call this render
return [hookSlots[currentIndex], setState];
}
// Simulating two renders of a component that calls useState twice:
resetHooksForRender();
const [name] = useState("Ada"); // reads/writes hookSlots[0]
const [age] = useState(36); // reads/writes hookSlots[1]
resetHooksForRender();
const [name2] = useState("Ada"); // hookSlots[0] again — SAME slot, because same call order
const [age2] = useState(36); // hookSlots[1] again