React Hooks Reference
Hooks are functions that let function components use React features like state, side effects, and context without writing a class. This lesson is a single-page reference: every commonly used built-in hook, what it returns, when to reach for it, and the mistakes that trip up almost everyone at some point. Treat it as both a first read and a page you come back to.
Overview: How Hooks Work
A function component is just a function that returns JSX. Every time React re-renders that component, the function runs again from the top. Normally that would mean any local variable resets to its initial value on every render — so how does a counter “remember” its value between renders? That’s exactly what hooks solve: they let a component keep state and side effects tied to it across renders, without the component becoming a class.
React does this by keeping a hidden, per-component list of “hook slots” attached to the component’s internal fiber (its entry in React’s internal tree). Each call to useState, useRef, useEffect, and so on claims the next slot in that list, in order. On the first render, React creates the slot and stores the initial value. On every subsequent render, React walks the same list in the same order and hands back the stored value for each slot.
This is exactly why the Rules of Hooks exist: hooks must be called in the same order on every render, and only at the top level of a component or another hook (never inside if, loops, or nested functions). If a hook call is skipped on some renders but not others, React’s slot list gets misaligned with the calls, and state gets attached to the wrong hook — silently corrupting your component.
Calling a state setter (like the second value from useState) or dispatching an action from useReducer schedules a re-render: React re-runs the component function, gets new JSX, diffs it against the previous JSX (reconciliation), and then commits only the minimal DOM changes needed. Hooks that read reactive values (useEffect, useMemo, useCallback) take a dependency array so React knows whether to re-run their logic after a given render.
Syntax
import { useState, useEffect, useRef, useContext, useReducer, useCallback, useMemo } from "react";
function MyComponent() {
const [state, setState] = useState(initialValue);
useEffect(() => { /* side effect */ return () => { /* cleanup */ }; }, [deps]);
const ref = useRef(initialValue);
const ctxValue = useContext(MyContext);
const [state2, dispatch] = useReducer(reducer, initialState);
const memoFn = useCallback(() => { /* ... */ }, [deps]);
const memoValue = useMemo(() => computeExpensive(a, b), [a, b]);
}
Every hook must be imported by name from "react" and called unconditionally at the top level of a function component (or a custom hook, which is just a function whose name starts with use).
Hook Reference Table
| Hook | Returns | Purpose |
|---|---|---|
useState |
[value, setValue] |
Local component state that persists across renders |
useEffect |
undefined |
Run side effects (fetching, subscriptions, timers) after render |
useRef |
{ current: value } |
Mutable value that persists across renders without causing re-render; also used for DOM node references |
useContext |
the current context value | Read a value from a Context.Provider without prop drilling |
useReducer |
[state, dispatch] |
State managed by a reducer function, for more complex state logic |
useCallback |
a memoized function | Keep the same function reference across renders unless dependencies change |
useMemo |
a memoized value | Skip an expensive recalculation unless dependencies change |
Examples
Example 1: useState — a counter
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
export default Counter;
This renders a button reading “Count: 0”. Each click calls setCount with the new value, which schedules a re-render; React re-runs Counter, gets the new JSX with the updated number, and patches only the text node that changed.
Example 2: useEffect — fetching data with cleanup
import { useState, useEffect } from "react";
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let cancelled = false;
async function loadUser() {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
if (!cancelled) setUser(data);
}
loadUser();
return () => {
cancelled = true;
};
}, [userId]);
if (!user) return <p>Loading...</p>;
return <p>{user.name}</p>;
}
export default UserProfile;
On mount, this renders “Loading…”, then the effect runs, fetches the user, and once the response arrives calls setUser, triggering a re-render that shows the user’s name. The userId dependency means that if the prop changes, React first runs the cleanup function (setting cancelled to true so a stale response is ignored) and then reruns the effect for the new id.
Example 3: useContext + useReducer — a theme toggle with shared state
import { createContext, useContext, useReducer } from "react";
const ThemeContext = createContext(null);
function themeReducer(state, action) {
switch (action.type) {
case "toggle":
return state === "light" ? "dark" : "light";
default:
return state;
}
}
function ThemeProvider({ children }) {
const [theme, dispatch] = useReducer(themeReducer, "light");
return (
<ThemeContext.Provider value={{ theme, dispatch }}>
{children}
</ThemeContext.Provider>
);
}
function ThemeToggleButton() {
const { theme, dispatch } = useContext(ThemeContext);
return (
<button onClick={() => dispatch({ type: "toggle" })}>
Current theme: {theme}
</button>
);
}
export default function App() {
return (
<ThemeProvider>
<ThemeToggleButton />
</ThemeProvider>
);
}
This renders a button reading “Current theme: light”. Clicking it dispatches a toggle action; the reducer computes the next theme, useReducer updates state, and every consumer of ThemeContext — here, ThemeToggleButton — re-renders with the new value, so the button text flips to “Current theme: dark”. This pattern avoids passing theme and a setter down through props manually.
Under the Hood: Mount, Update, Unmount
On mount, React runs the component function top to bottom, creating a fresh slot for each hook call in order, then commits the resulting DOM. useEffect callbacks run after the browser has painted, not during render.
On an update (triggered by a state or context change), React re-runs the function, reuses the existing slots in the same order, computes new JSX, and reconciles it against the previous tree. For each useEffect, React compares the new dependency array to the previous one (with Object.is per item); if any dependency changed, it runs the previous effect’s cleanup function first, then the new effect.
On unmount, React runs the cleanup function of every effect that has one, then discards the component’s hook slots entirely.
Common Mistakes
Mistake 1: Calling a hook conditionally.
function Panel({ isOpen }) {
if (isOpen) {
const [count, setCount] = useState(0); // breaks the Rules of Hooks
}
return <div />;
}
Skipping this hook on some renders shifts every hook call after it into the wrong slot. Always call hooks unconditionally, and put the condition inside the hook instead:
function Panel({ isOpen }) {
const [count, setCount] = useState(0);
if (!isOpen) return null;
return <div>{count}</div>;
}
Mistake 2: Omitting a dependency, causing a stale closure.
function Timer({ step }) {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + step); // "count" and "step" are captured from the first render
}, 1000);
return () => clearInterval(id);
}, []); // missing dependencies
return <p>{count}</p>;
}
Because the dependency array is empty, the effect only runs once, so the closure it creates always sees the original count and step — the counter increments once and then stalls. Fix it by including the real dependencies, or by using the updater form of setState to avoid needing count at all:
useEffect(() => {
const id = setInterval(() => {
setCount((prev) => prev + step);
}, 1000);
return () => clearInterval(id);
}, [step]);
Best Practices
- Always list every reactive value an effect reads in its dependency array — let the linter (
eslint-plugin-react-hooks) tell you when one is missing rather than guessing. - Prefer the updater form
setState(prev => ...)when the next state depends on the previous state, to avoid stale closures. - Extract repeated hook logic into a custom hook (a function starting with
use) instead of copy-pasting effects across components. - Don’t reach for
useMemooruseCallbackby default — only add them when you’ve identified an actual expensive computation or a child wrapped inmemothat re-renders unnecessarily. - Keep
useReducerfor state with multiple sub-values or complex transitions; keepuseStatefor simple, independent values. - Never mutate state directly — always call the setter with a new array or object (
setItems([...items, next])), even inside a reducer.
Practice Exercises
Exercise 1: Build a useToggle custom hook that returns [value, toggle], where toggle flips a boolean. Use it to show/hide a paragraph when a button is clicked.
Exercise 2: Write a component with a text <input> that uses useRef to focus the input automatically when the component mounts (hint: use useEffect with an empty dependency array and call ref.current.focus()).
Exercise 3: Convert a useState-based shopping cart (an array of items with add/remove) into a useReducer-based one with "add" and "remove" action types.
Summary
- Hooks let function components hold state and run side effects by claiming ordered “slots” React tracks per component — which is why they must be called unconditionally, in the same order, every render.
useStateanduseReducerhold state;useEffectruns side effects after paint and can clean up on dependency change or unmount.useRefholds a mutable value across renders without triggering re-renders, and is also used to reference DOM nodes.useContextreads shared values from aProviderwithout prop drilling.useCallbackanduseMemomemoize functions and values to avoid unnecessary work, but should be used only when there’s a measured benefit.- Always keep dependency arrays honest, and never mutate state directly.
