"Hooks can only be called inside a function component."
The rules and every violation.
React throws this error when a hook is called from a place that isn't a component or another hook. It's not a style check — React genuinely can't function otherwise. Hooks are identified by call order, and calling one from a nested function, an event handler, or outside a component breaks the bookkeeping. This page covers the rules, why they exist, and every common violation.
Short answer: Two rules. (1) Only call hooks at the top level — never in loops, conditions, or nested functions. (2) Only call from React components or custom hooks — never from regular functions, class methods, or async callbacks. Both exist because React identifies hooks by call order — the 3rd useState must always be the 3rd. Violating the rules makes this impossible to track, so React throws.
✅ Solution 1 — Only call hooks at the top level
Never inside conditions, loops, or nested functions.
This is the rule that catches the most bugs. If you wrap a hook in an if, React can't guarantee the same number of hooks runs every render. The useState that was the 2nd hook last render becomes the 3rd — or doesn't exist at all. Fix: move the hook call unconditionally to the top of the component, then use the value conditionally.
import { useState } from 'react'; // ─── BAD: hook inside a condition ──────────── function BadConditional({ enabled }: { enabled: boolean }) { if (enabled) { const [value, setValue] = useState(0); // ❌ Rules of hooks violation — hook count varies per render return{value}
; } returndisabled
; } // ─── GOOD: hook always called, render conditionally function GoodConditional({ enabled }: { enabled: boolean }) { // ✅ Always called — same hook order every render const [value, setValue] = useState(0); if (!enabled) { returndisabled
; } return{value}
; } // ─── BAD: hook inside a loop ───────────────── function BadLoop({ items }: { items: string[] }) { const states = items.map(item => { const [selected, setSelected] = useState(false); // ❌ Called conditionally — number of calls depends on items.length return { item, selected, setSelected }; }); return
- {states.map(s =>
- {s.item} )}
-
{items.map(item => (
- "checkbox" checked={selected.has(item)} onChange={() => toggle(item)} /> {item} ))}
-
{items.map(item => <ItemRow key={item} item={item} />)}
import { useState } from 'react'; function BadConditional({ enabled }) { if (enabled) { const [value, setValue] = useState(0); return React.createElement('p', null, value); } return React.createElement('p', null, 'disabled'); } function GoodConditional({ enabled }) { const [value, setValue] = useState(0); if (!enabled) { return React.createElement('p', null, 'disabled'); } return React.createElement('p', null, value); } function BadLoop({ items }) { const states = items.map(item => { const [selected, setSelected] = useState(false); return { item, selected, setSelected }; }); return React.createElement('ul', null, states.map(s => React.createElement('li', { key: s.item }, s.item))); } function GoodLoop1({ items }) { const [selected, setSelected] = useState(new Set()); function toggle(item) { setSelected(prev => { const next = new Set(prev); if (next.has(item)) next.delete(item); else next.add(item); return next; }); } return React.createElement('ul', null, items.map(item => React.createElement('li', { key: item }, React.createElement('input', { type: 'checkbox', checked: selected.has(item), onChange: () => toggle(item) }), item ) ) ); } function GoodLoop2({ items }) { return React.createElement('ul', null, items.map(item => React.createElement(ItemRow, { key: item, item })) ); } function ItemRow({ item }) { const [selected, setSelected] = useState(false); return React.createElement('li', null, React.createElement('input', { type: 'checkbox', checked: selected, onChange: e => setSelected(e.target.checked) }), item ); } console.log(BadConditional, GoodConditional, BadLoop, GoodLoop1, GoodLoop2, ItemRow);
Hooks are positional
The 1st useState is slot 1, the 2nd is slot 2. If call order changes, React maps slots wrong — the bug is silent and confusing.
Extract to child
In a loop, extract the hook-using logic into a child component. Each instance gets its own state slots.
Single state for many
Use one useState for a list of selections. One hook, all items, no rule violation.
Call unconditionally, use conditionally
Always call the hook. Then use its value in the conditional. That's the pattern that respects the rules.
✅ Solution 2 — Only call from components or custom hooks
Not from regular functions, class methods, or async callbacks.
The second rule: hooks must be called from inside a function component or a custom hook. Calling from an event handler, an async function, a utility, or a class method throws "Hooks can only be called inside the body of a function component." Fix: move the hook up to the component, pass the value down.
import { useState, useEffect } from 'react'; // ─── BAD: hook inside event handler ────────── function BadHandler() { function handleClick() { const [x, setX] = useState(0); // ❌ Rules of hooks — hooks can't be called from event handlers } return ; } // ─── GOOD: hook in component, use in handler function GoodHandler() { const [count, setCount] = useState(0); // ✅ top level function handleClick() { setCount(c => c + 1); // ✅ setter, not a hook } return ; } // ─── BAD: hook inside a regular function ───── function useBadHelper() { // This LOOKS like a custom hook (use prefix) but... const [x] = useState(0); return x; } function regularUtility() { // ❌ Not a hook, not a component — can't call hooks here const x = useBadHelper(); } // ─── GOOD: only call from components or hooks function ValidComponent() { const x = useBadHelper(); // ✅ inside component return{x}
; } function useValidHook() { const x = useBadHelper(); // ✅ inside another hook return x; } // ─── BAD: hook inside useEffect callback ───── function BadEffect() { useEffect(() => { const [x, setX] = useState(0); // ❌ The effect callback is not a component render }, []); return null; } // ─── GOOD: hook at top, effect uses it ─────── function GoodEffect() { const [data, setData] = useState<unknown>(null); // ✅ top level useEffect(() => { fetch('/api').then(r => r.json()).then(setData); }, []); return{JSON.stringify(data)}; } // ─── BAD: hook inside async function ───────── async function badAsync() { const [x] = useState(0); // ❌ Async functions aren't components } // ─── GOOD: async work in effect ────────────── function GoodAsync() { const [data, setData] = useState<unknown>(null); useEffect(() => { async function load() { const res = await fetch('/api'); setData(await res.json()); } load(); }, []); return{JSON.stringify(data)}; } // ─── Where hooks CAN be called ─────────────── // // ✅ Inside a function component: // function MyComponent() { useState(); ... } // // ✅ Inside a custom hook (name starts with 'use'): // function useMyHook() { useState(); ... } // // ✅ Inside another custom hook: // function useOuter() { useInner(); ... } // // ❌ Inside event handlers: // function onClick() { useState(); } // ❌ // // ❌ Inside useEffect callbacks: // useEffect(() => { useState(); }); // ❌ // // ❌ Inside regular functions: // function helper() { useState(); } // ❌ // // ❌ Inside class components: // class C { render() { useState(); } } // ❌ // // ❌ Inside async functions: // async function f() { useState(); } // ❌ // // ❌ Inside conditionals or loops: // if (x) { useState(); } // ❌ console.log(BadHandler, GoodHandler, useBadHelper, ValidComponent, useValidHook, BadEffect, GoodEffect, GoodAsync);
import { useState, useEffect } from 'react'; function BadHandler() { function handleClick() { const [x, setX] = useState(0); } return React.createElement('button', { onClick: handleClick }, 'Click'); } function GoodHandler() { const [count, setCount] = useState(0); function handleClick() { setCount(c => c + 1); } return React.createElement('button', { onClick: handleClick }, 'Count: ', count); } function useBadHelper() { const [x] = useState(0); return x; } function regularUtility() { const x = useBadHelper(); } function ValidComponent() { const x = useBadHelper(); return React.createElement('p', null, x); } function useValidHook() { const x = useBadHelper(); return x; } function BadEffect() { useEffect(() => { const [x, setX] = useState(0); }, []); return null; } function GoodEffect() { const [data, setData] = useState(null); useEffect(() => { fetch('/api').then(r => r.json()).then(setData); }, []); return React.createElement('pre', null, JSON.stringify(data)); } async function badAsync() { const [x] = useState(0); } function GoodAsync() { const [data, setData] = useState(null); useEffect(() => { async function load() { const res = await fetch('/api'); setData(await res.json()); } load(); }, []); return React.createElement('pre', null, JSON.stringify(data)); } console.log(BadHandler, GoodHandler, useBadHelper, ValidComponent, useValidHook, BadEffect, GoodEffect, GoodAsync);
Hooks during render only
Hooks must run synchronously during render. Event handlers, effects, and async functions run after — too late.
Setters aren't hooks
setCount from useState is a stable function, not a hook. Calling it in an event handler is fine — calling useState again is not.
Extract custom hooks
If you need to share hook logic, extract it into a function named useX. That function can call hooks — regular functions can't.
Async work goes in effects
async function can't be a component. Define async functions inside useEffect, or use a library (React Query, SWR).
React's rules-of-hooks ESLint plugin catches every violation before runtime. Install eslint-plugin-react-hooks and enable its two rules (rules-of-hooks and exhaustive-deps). The plugin analyzes your code statically and flags any violation with the exact line. Without it, you find violations at runtime, in production, in rare branches. With it, you find them in your editor. This is the single most valuable ESLint rule for React code.
✅ Solution 3 — The use prefix for custom hooks
How React and ESLint know a function is a hook.
Custom hooks must start with use. It's not a convention — it's how React identifies hooks for linting and how ESLint knows to apply the rules-of-hooks checks. Rename your hook without the prefix and the linter goes silent — real bugs slip through.
import { useState, useEffect } from 'react'; // ─── BAD: hook without use prefix ──────────── function getCounter() { const [count, setCount] = useState(0); // ⚠️ This is still a hook (calls useState) // But ESLint won't know — no 'use' prefix // Rules-of-hooks checks won't apply return { count, setCount }; } // ─── GOOD: use prefix ──────────────────────── function useCounter() { const [count, setCount] = useState(0); // ✅ ESLint recognizes this as a hook // Rules-of-hooks checks apply // Anything inside follows the rules return { count, setCount }; } // ─── Why the prefix matters ────────────────── // // ESLint rules-of-hooks plugin: // - Identifies hooks by name (starting with 'use') // - Applies rules to function bodies that are hooks // - Flags violations (conditional calls, wrong scope) // // Without 'use' prefix: // - ESLint treats the function as a regular function // - Doesn't check for hook rule violations // - The bugs surface at runtime instead of lint time // ─── Composite hooks keep the prefix ───────── function useUserProfile(userId: string) { // ✅ useCounter keeps the prefix — ESLint knows to check const { count } = useCounter(); const [user, setUser] = useState<unknown>(null); useEffect(() => { fetch(/api/users/${userId}`).then(r => r.json()).then(setUser); }, [userId]); return { count, user }; } // ─── Naming patterns ───────────────────────── // // ✅ useCounter — noun // ✅ useLocalStorage — feature // ✅ useDebounce — verb // ✅ useFetch — action // ✅ useDebouncedFetch — composed // ✅ useAuth — domain // // ❌ fetchUser — sounds like a regular function // ❌ getLocalStorage — 'get' implies plain function // ❌ counter — missing prefix // ❌ stateCounter — confusing // ─── When to make a custom hook ────────────── // // Make it a hook if: // ✅ It uses useState, useEffect, or any other hook // ✅ It encapsulates stateful logic // ✅ You'll reuse it in multiple components // ✅ It has a clear "one thing" it does // // Keep it a regular function if: // ❌ It doesn't use any hooks // ❌ It's a pure calculation (formatDate, slugify) // ❌ It doesn't need component state console.log(getCounter, useCounter, useUserProfile);
import { useState, useEffect } from 'react'; function getCounter() { const [count, setCount] = useState(0); return { count, setCount }; } function useCounter() { const [count, setCount] = useState(0); return { count, setCount }; } function useUserProfile(userId) { const { count } = useCounter(); const [user, setUser] = useState(null); useEffect(() => { fetch(/api/users/${userId}`).then(r => r.json()).then(setUser); }, [userId]); return { count, user }; } console.log(getCounter, useCounter, useUserProfile);
ESLint identifies by prefix
The rules-of-hooks plugin only analyzes functions starting with use. Without the prefix, no linting.
Prefix propagates
Custom hooks that use other hooks keep the prefix — so a chain of hook calls stays linted throughout.
Naming conventions
useCounter, useFetch, useLocalStorage, useDebounce — noun/verb after use.
Hook or function
Uses hooks? Make it useX. Pure calculation? Regular function. The choice is mechanical.
⚠️ Common mistakes
The rules-of-hooks mistakes that cost real time.
if blockVarying hook count per render. Fix: call unconditionally, use conditionally.
Count depends on array length. Fix: one hook with an array, or extract a child component.
function helper() { useState(); } — not a component or hook. Fix: move to top level, pass values down.
useEffect(() => { useState(); }) — the callback isn't a render. Fix: state at top, effect uses it.
onClick={() => { useState(); }} — handlers aren't render. Fix: state at top, setter in handler.
async function f() { useState(); } — async isn't a render. Fix: async work goes in effect.
use prefixESLint skips checking. Rules-of-hooks bugs go uncaught. Fix: prefix every custom hook.
if (!user) return null; const [x] = useState(); — hook skipped conditionally. Fix: all hooks first, then early return.
❓ Frequently asked
What does "Hooks can only be called inside the body of a function component" mean?
You called a hook (useState, useEffect, etc.) from a place that isn't a function component or another hook. React hooks rely on call order — each render, hooks are identified by the sequence they're called in. Calling a hook from a regular function, an event handler, a class method, or outside a component breaks the tracking.
What are the two rules of hooks?
Rule 1: Only call hooks at the top level. Never inside loops, conditions, or nested functions. Rule 2: Only call hooks from React function components or custom hooks. Never from regular JS functions, class components, or utility functions.
Why can't I call hooks conditionally?
React tracks hooks by call order. If you call useState inside an if block, React can't know whether to allocate a new state slot or reuse the old one — the count of hooks would differ between renders. Fix: call hooks unconditionally at the top level, then use the resulting state or value conditionally inside the render.
Can I call a hook inside useEffect or an event handler?
No. useEffect callbacks and event handlers run after the component renders — they're not part of the render. Hooks must be called during render, at the top level. If you need state inside an effect, capture it via closure or a ref. If you need a state update in an event, use the setter (setCount) — that's not a hook, it's a function the hook returned.