Hooks Overview & Rules
Hooks are plain functions that let a function component “hook into” React features that used to require class components — local state, side effects, context, refs, and more. Every hook name starts with use, and calling one inside a component subscribes that component to a slice of React’s machinery. Since they arrived in React 16.8 they have become the standard way to write React, and modern code (React 18/19) is built almost entirely from function components plus hooks. Understanding what hooks are, the two rules that govern them, and which hook does what is the foundation for everything else in this section.
What a hook actually is
A hook is just a function you call at the top of a component (or inside another hook). It returns some value — current state, a memoized callback, a ref object — and quietly registers the component with React so it can re-render when that value changes. Because hooks rely on React tracking calls in order, they only work inside a React render: a function component body, or a custom hook that is itself called from one.
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount((c) => c + 1)}>
Clicked {count} times
</button>
);
}
Here useState gives the component a piece of state and a setter. React remembers the value between renders even though the function runs fresh every time. That memory is the whole point of hooks.
The two Rules of Hooks
Hooks come with exactly two rules. They are not stylistic preferences — break them and React loses track of state and crashes or behaves unpredictably.
1. Only call hooks at the top level. Never call a hook inside a loop, condition, nested function, or after an early return. Call them in the same order on every render.
2. Only call hooks from React functions. Call them from function components or from custom hooks — never from regular JavaScript functions, event handlers, or class components.
function Profile({ userId }) {
// ❌ Wrong: conditional hook changes call order between renders
if (userId) {
const [name, setName] = useState("");
}
// ✅ Right: always call unconditionally, branch on the value instead
const [name, setName] = useState("");
if (!userId) return <p>Not signed in</p>;
return <h1>{name}</h1>;
}
Why the order matters
React does not match hooks by name; it matches them by call order. On the first render it builds an internal list — first useState, then useEffect, and so on — and on every later render it walks that same list position by position to hand back the right value. If a hook is skipped one render because it sat behind an if, every hook after it shifts by one slot and gets the wrong state.
Think of hooks as a numbered list React reads top to bottom. Skipping item 2 makes item 3 grab item 2’s data. Keeping calls unconditional and in a fixed order is what keeps that list aligned.
The ESLint plugin
You should not police these rules by hand. The official eslint-plugin-react-hooks flags both violations automatically and also checks that effect dependency arrays are complete. It ships in every Vite and Create React App template, but installing it manually is trivial.
npm install --save-dev eslint-plugin-react-hooks
// eslint.config.js (flat config)
import reactHooks from "eslint-plugin-react-hooks";
export default [
{
plugins: { "react-hooks": reactHooks },
rules: {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
},
},
];
The rules-of-hooks rule enforces the two rules above; exhaustive-deps warns when an effect or memo references a value it forgot to list as a dependency — the single most common source of stale-closure bugs.
Map of the built-in hooks
React ships a focused set of built-in hooks. Each solves one job; learning when to reach for which is most of the skill. The table below maps every common hook to its purpose and links to its dedicated page.
| Hook | Job |
|---|---|
useState | Local component state with a setter that triggers re-renders |
useReducer | State managed through a reducer — better for complex transitions |
useEffect | Run side effects (fetches, subscriptions) after the render commits |
useLayoutEffect | Like useEffect but fires synchronously before the browser paints |
useRef | A mutable box that persists across renders without causing re-renders |
useContext | Read a context value provided higher in the tree |
useMemo | Cache an expensive computed value between renders |
useCallback | Cache a function identity so children don’t re-render needlessly |
useId | Generate stable unique IDs for accessibility attributes |
useTransition | Mark state updates as non-urgent so the UI stays responsive |
useDeferredValue | Defer re-rendering of a value to keep input snappy |
useImperativeHandle | Customize the ref a parent receives from a child |
useSyncExternalStore | Subscribe safely to an external (non-React) store |
use | Read a promise or context conditionally during render (React 19) |
Beyond these, you compose hooks into your own custom hooks — functions that bundle several built-in hooks behind a clean API, like useFetch or useLocalStorage.
Best Practices
- Call every hook unconditionally at the very top of the component, before any early return.
- Treat the
react-hooksESLint rules as errors in CI — they catch order and dependency bugs you cannot see by reading. - Name custom hooks with a
useprefix so the linter recognizes and checks them. - Reach for the narrowest hook that fits: prefer
useStateuntil reducers genuinely simplify the logic. - Keep effects focused — one concern per
useEffect— and always return a cleanup function when you subscribe to anything. - Don’t add
useMemo/useCallbackreflexively; apply them when profiling shows a real re-render cost. - Extract repeated hook combinations into custom hooks instead of copy-pasting logic between components.