Problem Solved

React · rules of hooks

Home › Courses › React › Problems › Rules of hooks
⚛️ Problem · 5 min read

"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.

top-level.tsx
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}

; } return

disabled

; } // ─── 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) { return

disabled

; } 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}
  • )}
; } // ─── GOOD 1: single state for all items ────── function GoodLoop1({ items }: { items: string[] }) { const [selected, setSelected] = useState<Set<string>>(new Set()); function toggle(item: string) { setSelected(prev => { const next = new Set(prev); if (next.has(item)) next.delete(item); else next.add(item); return next; }); } return (
    {items.map(item => (
  • "checkbox" checked={selected.has(item)} onChange={() => toggle(item)} /> {item}
  • ))}
); } // ─── GOOD 2: extract child component ───────── function GoodLoop2({ items }: { items: string[] }) { return (
    {items.map(item => <ItemRow key={item} item={item} />)}
); } function ItemRow({ item }: { item: string }) { const [selected, setSelected] = useState(false); // ✅ Each ItemRow has its own state, called once per mount return (
  • "checkbox" checked={selected} onChange={e => setSelected(e.target.checked)} /> {item}
  • ); } // ─── The rule ──────────────────────────────── // // Hooks are identified by CALL ORDER, not by name: // Render 1: useState → useState → useEffect // Render 2: useState → useState → useEffect // ↑ ↑ ↑ // slot 1 slot 2 slot 3 // // If you wrap a hook in a condition, the count differs: // Render 1: useState → useState → useState (3 calls) // Render 2: useState → useEffect (2 calls) // ↑ // React thinks slot 2 is now useEffect // → state mismatch → bug // // That's why hooks must be top-level and unconditional. console.log(BadConditional, GoodConditional, BadLoop, GoodLoop1, GoodLoop2, ItemRow);
    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.

    component-only.tsx
    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.

    use-prefix.tsx
    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.

    1. Hook inside an if block

    Varying hook count per render. Fix: call unconditionally, use conditionally.

    2. Hook inside a loop

    Count depends on array length. Fix: one hook with an array, or extract a child component.

    3. Hook inside a regular function

    function helper() { useState(); } — not a component or hook. Fix: move to top level, pass values down.

    4. Hook inside useEffect callback

    useEffect(() => { useState(); }) — the callback isn't a render. Fix: state at top, effect uses it.

    5. Hook inside an event handler

    onClick={() => { useState(); }} — handlers aren't render. Fix: state at top, setter in handler.

    6. Calling a hook from an async function

    async function f() { useState(); } — async isn't a render. Fix: async work goes in effect.

    7. Custom hook without use prefix

    ESLint skips checking. Rules-of-hooks bugs go uncaught. Fix: prefix every custom hook.

    8. Early return before hooks

    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.

    🔗 Related

    ← All React problems
    Problem solved · React