Devpuff
Pricing
Log InStart Free
  1. Home
  2. Blog
  3. React hooks explained, the complete map
Learn

React hooks explained, the complete map

React ships around seventeen hooks and you need five. Here is what each one is for, the two rules that break everything, and how to pick the right one.

By Max Arthur
Co-Founder & Content Marketer·September 10, 2026·9 Min read
White text reading 'React hooks explained' on a dark purple dotted background

React hooks are functions that let a function component remember things and reach outside itself. useState gives a component memory between renders. useEffect connects it to something outside React. Every hook name starts with use, and that prefix is not decoration: it is how React and your linter identify them.

There are around seventeen built-in hooks. You will use five.

Hook What it gives you How often you need it
useState A value that survives re-renders Constantly
useEffect A way to sync with something outside React Often
useContext Data from an ancestor without passing props Often
useRef A value that changes without causing a render Sometimes
useMemo A cached calculation When something is slow
useCallback A cached function identity When something is slow
The other 11 Libraries, edge cases, and specific problems Rarely

This post maps all of them, but spends its time on the six above, because that is where the work is.

Why hooks exist at all

Before React 16.8, only class components could hold state. A function component was a rendering function and nothing else, so the moment a piece of UI needed to remember one thing, you rewrote it as a class.

Hooks removed that fork. Function components can now do everything, and the class syntax is legacy. If a tutorial opens with class App extends React.Component, it is teaching you the old API.

The second reason matters more day to day. In a class, logic for one feature got scattered across componentDidMount, componentDidUpdate and componentWillUnmount. Hooks let you put all the logic for one concern in one place, and then lift it into a custom hook and reuse it.

useState: memory between renders

import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  return <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>;
}

useState returns exactly two things: the current value and a function to change it. The argument is the starting value, used only on the first render.

Three things that trip people up.

Setting state does not change the variable you are holding. It schedules a re-render, and the new value shows up in the next one. Logging count right after setCount shows the old value. That is correct behaviour, not a bug.

Multiple updates in one event batch together. Calling setCount(count + 1) three times in a row adds one, not three, because all three read the same stale count. When the new value depends on the old one, pass a function:

setCount((c) => c + 1); // this form stacks correctly

State updates must be immutable. Pushing into an array in state does not re-render, because the array is the same object and React compares by identity:

items.push(newItem);      // no re-render
setItems([...items, newItem]); // correct

When state gets complicated enough that you are writing several related useState calls that always change together, that is the signal to switch to useReducer, which puts the update logic in one function instead of scattered across handlers.

useEffect: talking to the world outside React

useEffect runs code after render, and it exists for one purpose: synchronising your component with a system React does not control. Network requests, subscriptions, timers, browser APIs, direct DOM work.

useEffect(() => {
  const id = setInterval(() => setTime(Date.now()), 1000);
  return () => clearInterval(id); // cleanup
}, []);

The second argument is the dependency array, and it decides when the effect re-runs. [] means once on mount. [userId] means whenever userId changes. Leaving it out entirely means after every single render, which is how infinite loops get written.

The returned function is cleanup, and skipping it is the most common real bug in React code. An interval you never clear keeps firing after the component is gone.

The dependency array and the infinite-loop failure modes have enough detail to deserve their own article, so they get one: useEffect Explained covers dependencies, cleanup, and the four ways effects loop forever.

One correction worth absorbing early: most useEffect calls people write should not exist. Deriving a value from state does not need an effect, it needs a plain variable. Responding to a click does not need an effect, it needs the click handler. React's own guidance has a page titled "You Might Not Need an Effect", and it is worth reading before you reach for the hook.

useContext: skipping the prop chain

When a value has to reach a component six levels down, passing it through every layer in between is called prop drilling. Context is the way out.

const ThemeContext = createContext("light");

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Page />
    </ThemeContext.Provider>
  );
}

function DeepButton() {
  const theme = useContext(ThemeContext); // "dark", no props involved
  return <button className={theme}>Save</button>;
}

useContext reads the nearest matching Provider above it. If there is no Provider, it uses the default you passed to createContext.

The cost to know about: every component reading a context re-renders when that context value changes. Putting one big object holding everything into a single context means a change to any part of it re-renders every consumer. Split contexts by how often they change, not by what they are about.

Context is for genuinely global-ish things: the current user, theme, language. It is not a state management library, and it is not a performance optimisation.

useRef: a value that does not trigger renders

useRef gives you a mutable box that survives re-renders and, unlike state, changing it renders nothing.

Two distinct jobs.

Reaching a DOM node:

const inputRef = useRef(null);

useEffect(() => {
  inputRef.current.focus();
}, []);

return <input ref={inputRef} />;

Holding a value between renders that the UI does not display, such as a timer id or the previous value of a prop:

const timerRef = useRef(null);
timerRef.current = setTimeout(...); // changing this renders nothing

The test for choosing between useRef and useState is simple: if changing it should update what the user sees, it is state. If it should not, it is a ref. Storing display data in a ref is the mistake, and its symptom is a screen that will not update.

useMemo and useCallback: caching, with a caveat

Both cache something between renders. useMemo caches a computed value, useCallback caches a function.

const sorted = useMemo(() => hugeList.sort(compare), [hugeList]);
const handleClick = useCallback(() => submit(id), [id]);

The caveat is that both cost something. They add code, they add a dependency array to keep correct, and they run on every render to check whether the cache is still valid. Wrapping a cheap calculation makes the component slower and harder to read.

Use them when you have measured a problem: a genuinely expensive calculation, or a function or object being passed to a child wrapped in React.memo, where a new identity every render defeats the memo.

Reach for them second, after you have checked whether the component needs to re-render at all.

The two rules, and what breaks

React documents these as the Rules of Hooks, and they are not style preferences.

1. Only call hooks at the top level. Never inside a condition, loop, or nested function.

// Broken
if (isLoggedIn) {
  const [name, setName] = useState("");
}

// Correct
const [name, setName] = useState("");
if (isLoggedIn) { /* use it */ }

React does not know your hooks by name. It tracks them by call order, so the first useState on one render must be the same first useState on the next. Put one behind an if and the order shifts, and React hands your state to the wrong hook. The error you get, "Rendered fewer hooks than expected", is React noticing the count changed.

2. Only call hooks from React function components or other custom hooks. Not from plain utility functions, not from class components, not from event handlers.

The eslint-plugin-react-hooks package catches both automatically, and it is worth installing on day one rather than learning these rules from a production bug.

Custom hooks: the actual payoff

A custom hook is a function whose name starts with use and which calls other hooks. That is the entire specification. No API, no registration.

function useLocalStorage(key, initial) {
  const [value, setValue] = useState(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initial;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

Any component can now call const [draft, setDraft] = useLocalStorage("draft", "") and get persistence for free.

Custom hooks share logic, not state. Two components calling useLocalStorage get two independent pieces of state that happen to behave the same way. That surprises people who expect a hook to be a store. It is not; it is a recipe.

This is the feature classes never had a clean answer for, and it is the real reason hooks won.

The full map

Grouped the way React's own reference groups them.

Group Hooks When you touch it
State useState, useReducer Every component with memory
Context useContext Themes, current user, locale
Refs useRef, useImperativeHandle DOM access; the second is rare
Effects useEffect, useLayoutEffect, useInsertionEffect, useEffectEvent Outside systems; the last two are for libraries
Performance useMemo, useCallback, useTransition, useDeferredValue After you measure a problem
Other useId, useDebugValue, useSyncExternalStore, useActionState Accessibility ids, devtools, store adapters

useLayoutEffect is worth one line of explanation because it is the one people occasionally need: it runs before the browser paints, so use it when reading layout and then changing it would otherwise flicker. It blocks painting, so useEffect stays the default.

Picking the right hook

Work down this list until something matches.

  • Should the screen change when this value changes? useState.
  • Do several pieces of state always change together? useReducer.
  • Does this value need to survive renders but never trigger one? useRef.
  • Do I need a DOM node? useRef.
  • Am I syncing with something outside React? useEffect, and check whether you need it at all first.
  • Is this value needed far down the tree? useContext.
  • Have I measured something slow? useMemo or useCallback.
  • Am I repeating the same hook combination in several components? Write a custom hook.

Build something with them

Hooks are one of those topics where reading the list gives you the vocabulary and none of the instinct. The instinct comes from writing a component that re-renders when it should not and finding out why.

React Essentials builds from useState to custom hooks with the components rendering live next to the code, so a stale-state bug is something you fix rather than something you read about.

Related reading: useEffect Explained for the dependency array in full, JavaScript Closures Explained because stale state in a hook is a closure capturing an old render, and Async Await Explained for fetching data without the sequential trap.

Keep reading

More from the blog

White text reading 'How range() works in Python' on a dark purple dotted background
October 12, 2026·8 min readLearn

range() in Python: start, stop, step, and why it is lazy

range(1000000) uses 24 bytes. A list of a thousand numbers uses 4,028. Here is what range actually is, measured.

Read more
White text reading 'How to convert a string to a number in JavaScript' on a dark purple dotted background
October 3, 2026·9 min readLearn

How to convert a string to a number in JavaScript

Converting a string to a number in JavaScript: an empty string is 0 with Number and NaN with parseInt. What four methods do to awkward input.

Read more
{ }
✦

Ready to write some code?

Put this into practice - start your first free lesson. No setup, no credit card.

Start learning free
Devpuff

Learn to code by doing. One tiny, playful lesson at a time.

Learn
ProgramsCoursesPricing
Company
AboutBlogResourcesAffiliates
Support
Help CenterContactStatus
Programs
Frontend DevelopmentFull-Stack DevelopmentReact DeveloperPython Developer
Courses
Advanced ReactAsync JavaScriptAsync PythonCSS BasicsCSS LayoutDSA Basics
Learn to Code
Learn JavaScriptLearn PythonLearn SQLLearn HTMLLearn CSSLearn ReactBrowse All Topics
Platform Comparisons
Devpuff vs CodecademyDevpuff vs Mimo
© 2026 Devpuff. All rights reserved.Privacy PolicyTerms and ConditionsCookies PolicyRefund Policy
Devpuff vs Sololearn
Devpuff vs freeCodeCamp
Read All Comparisons