← All articles
HooksPracticeInterview Prep

React Hooks Exercises: A Practice Roadmap

Not another list of hook definitions. This is a practice roadmap — the mental model, the mistake almost everyone makes, and a real coding challenge for every major React hook.

Kumar Astik· MERN Developer12 min read

Searching "react hooks exercises" usually turns up one of two things: a wall of hook definitions with no code to actually run, or a practice site that hands you problems with zero explanation of what you're supposed to be learning from them. Neither one tells you what to practice, in what order, or what mistake you're actually trying to catch yourself making.

This is a roadmap, not another definitions list. For each major hook: the mental model in a couple of sentences, the mistake that trips people up even after they've read the docs, and a real coding challenge — with hidden tests — to go confirm you've actually got it.

How to use this guide

Go in order. Each section below takes maybe two minutes to read, but the point isn't the reading — it's clicking through to the linked problem and writing the code yourself before you decide you already know it. If a mistake described below doesn't feel familiar, that's a good sign; if it does, that's exactly the gap the problem is built to close.

useState

The mental model: useState gives a component a piece of data it owns and a way to trigger a re-render when that data changes. The part people skip past is that calling the setter doesn't mutate anything in place — it schedules a new render with the new value.

The mistake: updating object or array state by mutating it directly instead of creating a new reference, then wondering why the UI doesn't update.

// Looks reasonable, does nothing visible
function addItem(item) {
  cart.items.push(item); // mutates the existing array
  setCart(cart);          // same reference — React sees no change
}

// Works, because it's a new reference
function addItem(item) {
  setCart((prev) => ({ ...prev, items: [...prev.items, item] }));
}

Practice it against real tests: start with a counter that has to handle a reset button correctly, then move to a todo list where you're adding, toggling, and deleting items — that's where the object-mutation mistake above actually shows up.

  • Counter with reset — the smallest possible useState problem, good for a first pass
  • Todo List — CRUD with useState — where array-mutation bugs actually surface
  • Accordion — only one section open at a time — state that controls more than one visual element at once

useEffect

The mental model: useEffect lets a component synchronize with something outside of React — the DOM, a timer, a subscription, browser storage. It is not a general-purpose "run this after render" hook, even though it's easy to start using it that way.

The mistake: getting the dependency array wrong in either direction — omitting a value that's actually used inside the effect (stale closures), or forgetting the cleanup function and leaving timers, listeners, or subscriptions running after the component unmounts.

// Missing cleanup — this interval keeps running after unmount
useEffect(() => {
  const id = setInterval(() => setSeconds((s) => s + 1), 1000);
}, []);

// Correct — cleanup tears down what the effect set up
useEffect(() => {
  const id = setInterval(() => setSeconds((s) => s + 1), 1000);
  return () => clearInterval(id);
}, []);
"If your effect sets something up, it almost always needs to be the one that tears it down."

This is the exact useEffect practice problem to start with — sync a count to the document title, and the hidden tests will catch a wrong dependency array or missing cleanup immediately. Try the useEffect Practice challenge →

Once that one passes cleanly, conditional rendering is a good next stop — it's not a hooks problem on the surface, but it's usually solved wrong by people who reach for an effect to hide UI instead of just deriving it from state during render.

  • Conditional Rendering — show and hide UI based on state, without reaching for an unnecessary effect

useRef

The mental model: useRef gives you a mutable box that persists across renders without causing a re-render when it changes. That's the whole feature — persistence without re-rendering.

The mistake: reaching for useRef to hold data that actually needs to show up in the UI. If you change a ref's .current value and expect the screen to update, you'll be confused for a while — refs don't trigger renders, by design.

  • usePrevious Hook — track previous value, a small custom hook built entirely on this exact behavior of refs

useContext

The mental model: useContext lets a component read a value provided further up the tree without threading it through every component in between as props.

The mistake: putting everything into one giant context — user, theme, cart, notifications — so that any update to any one of them re-renders every single consumer of the context, even the ones that only cared about a field that didn't change.

  • Toggle theme provider — the smallest version of the pattern
  • Auth & Theme with Context API — compose providers yourself, where you actually feel the cost of one big context versus several scoped ones

useReducer

The mental model: useReducer moves "what changes and how" out of scattered setState calls and into one function that takes the current state and an action, and returns the next state. It earns its complexity once state stops being a couple of independent primitives and starts being a small system with rules.

The mistake: reaching for useReducer immediately out of habit, on state that's simple enough that two or three useState calls would be more readable — or the opposite, sticking with a pile of useState calls long after the update logic between them has started depending on each other.

  • Shopping Cart — managing complex state with useReducer, where actions (add, remove, update quantity) map naturally onto reducer cases
  • Multi-step wizard state — state transitions that depend on which step you're currently on

useMemo and useCallback

The mental model: both exist to preserve a reference between renders — useMemo for a computed value, useCallback for a function — so that a child component receiving it as a prop doesn't see a "new" value on every render and re-render unnecessarily.

The mistake: reaching for either one before measuring anything. Memoizing a cheap calculation or a function that isn't passed to a memoized child adds overhead for zero benefit — the fix should follow a profiler telling you where the actual cost is, not a reflex.

  • Stop wasteful list re-renders — where memoization visibly matters
  • Stable callback identities — the useCallback half of the same problem
  • Virtualize a 10k-row table — a case where memoization alone won't save you, and the real fix is windowing

Custom hooks

The mental model: a custom hook is just a function that calls other hooks and packages up reusable stateful logic — it's a naming convention and an extraction pattern, not a new primitive.

The mistake: going too far in one direction — never extracting repeated effect/state logic into a hook at all, or over-abstracting a single-use piece of logic into a "reusable" hook that only one component ever calls.

  • useDebounce Custom Hook — the classic first custom hook, and a good test of whether you actually understand cleanup
  • useInterval with pause — a slightly harder version of the same idea
  • useFetch with in-memory cache — where a custom hook starts doing real work worth extracting

React hooks interview questions (quick-fire)

A handful of hook questions come up often enough in interviews that they're worth being able to answer cold, without needing to think:

Why can't you call a hook inside a condition or a loop?

React tracks hooks by call order, not by name, to match each useState or useEffect call to its stored state between renders. Calling a hook conditionally can change that order between renders, which desyncs React's internal bookkeeping from your component's actual state.

What's the difference between useEffect and useLayoutEffect?

useEffect runs after the browser paints. useLayoutEffect runs synchronously before paint — useful when you need to measure or adjust the DOM without a visible flicker, but it blocks painting, so it's the exception, not the default.

Does calling useState's setter always trigger a re-render?

Only if the new value is different from the current one by reference (for objects/arrays) or value (for primitives). Setting state to the exact same primitive value it already holds is a no-op — React bails out without re-rendering.

Why does a stale value show up inside an effect or callback?

Closures inside effects and callbacks capture the values from the render they were created in. If a dependency isn't listed and the effect doesn't re-run, it keeps referencing the old value — this is almost always a missing entry in the dependency array.

A suggested practice order

If you're working through this roadmap top to bottom instead of jumping around, here's a reasonable path from first problem to hardest:

  • 1. Counter with reset → Todo List CRUD → Accordion (useState)
  • 2. useEffect Practice: sync count to document title → Conditional Rendering (useEffect)
  • 3. usePrevious Hook (useRef)
  • 4. Toggle theme provider → Auth & Theme with Context API (useContext)
  • 5. Shopping Cart with useReducer → Multi-step wizard state (useReducer)
  • 6. useDebounce → useInterval with pause → useFetch with in-memory cache (custom hooks)
  • 7. Stop wasteful list re-renders → Stable callback identities → Virtualize a 10k-row table (performance)
"You don't rise to the level of your goals. You fall to the level of your systems."James Clear

None of this sticks from reading it once. The point of listing the mistake next to each hook is so you notice it happening in your own code — and the only way that happens is by writing the code, not by recognizing the answer when you see it.

Ready to work through the full list? Browse all React coding challenges →

And if you're prepping for an interview specifically rather than practicing generally, the senior-level guide goes deeper on judgment questions beyond hook syntax.

See what a senior-level React interview actually tests. Read the senior React interview questions guide →

Frequently asked questions

What are good react hooks exercises for beginners?

Start with useState on something small and visual — a counter with reset, a todo list, an accordion that only opens one section at a time. Once those feel automatic, move to useEffect exercises that involve cleanup, like syncing a value to the document title.

How do I practice useEffect specifically?

Practice on tasks that force you to think about the dependency array and cleanup function, not just the effect body — syncing document title to state, setting up and tearing down a subscription, or debouncing a value are all good useEffect practice exercises because they break in specific, visible ways when you get the dependencies wrong.

Are React hooks asked in interviews?

Yes, consistently — useState and useEffect are close to guaranteed in a frontend interview, and useMemo, useCallback, useContext, and useReducer show up often enough that you should be able to explain not just what they do but when you'd reach for them over the simpler alternative.

What order should I practice React hooks in?

useState and useEffect first, since almost everything else builds on them. Then useRef and useContext. Then useReducer once state logic gets complex enough that useState becomes awkward. Save useMemo, useCallback, and custom hooks for last — they're optimizations and abstractions on top of patterns you should already be comfortable with.

Do I need to memorize hook syntax to pass an interview?

No — interviewers care far more about whether you know when a hook is the right tool and what breaks when it's used carelessly (a missing dependency, a ref that doesn't trigger a re-render) than whether you can recite the exact function signature from memory.

About the author
Kumar AstikMERN Developer
Keep reading