Suggest an editImprove this articleRefine the answer for “Rules for using hooks in React”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**React hooks rules** require calling Hooks only at the top level of a component or custom Hook, and only from functional components or custom Hooks, not from plain functions or event handlers. ```tsx // Wrong if (condition) { useState(0); } // Correct const [count, setCount] = useState(0); if (condition) { /* use count */ } ``` **Key point:** React tracks Hooks by call order. Same order every render keeps state stable.Shown above the full answer for quick recall.Answer (EN)Image**React Hooks rules** define where and how you can call functions like `useState` and `useEffect` inside your app. Break them and React loses track of which state belongs to which component. ## Theory ### TL;DR - Call Hooks only at the top level of a component or custom Hook, never inside conditions, loops, or nested functions - Call Hooks only from React functional components or custom Hooks (functions starting with `use`) - React tracks Hooks by call position, not by name. Same order every render = stable state - Analogy: a restaurant that prepares dishes in a fixed sequence. Skip one step and the kitchen loses track of your whole order - Decision rule: write all Hook calls first, then add conditions and loops below them ### Quick example ```tsx // BAD: conditional hook call breaks the order function BadComponent({ showCount }: { showCount: boolean }) { if (showCount) { const [count, setCount] = useState(0); // React loses slot tracking } return <div>{showCount ? count : 'Hidden'}</div>; } // GOOD: hook always runs, condition comes after function GoodComponent({ showCount }: { showCount: boolean }) { const [count, setCount] = useState(0); // Slot 1 - always called return <div>{showCount ? count : 'Hidden'}</div>; } ``` `BadComponent` throws "Rendered more hooks than during the previous render". `GoodComponent` works on every render. ### Why call order matters React stores Hook state in a linked list attached to each component's fiber node. On the first render it builds that list: slot 1, slot 2, slot 3. On every re-render it walks the same list in the same order to restore each value. If a conditional skips slot 1, slot 2 gets data meant for slot 1 and everything shifts. The rule isn't arbitrary. It's a direct result of how React's fiber architecture works internally. ### When to use - **Need state or a side effect in a component** - add a Hook at the top, before any `return` statement - **Same stateful logic in multiple components** - extract into a custom Hook named `useMyLogic`, not a plain function - **Conditional logic needed** - put the condition inside the Hook call or after all Hook calls - **Loops** - call one Hook outside the loop, store multiple values in an array or object keyed by index - **Event handlers** - define them inside the component, but never call Hooks inside the handler body itself Early returns are fine. `if (!userId) return null` placed after your `useState` calls is valid React 18 code. Hooks just need to complete before any conditional exits. ### How React detects violations React's internal dispatcher switches between `mount` and `update` phases per render. During `update`, it checks that the number of Hook calls matches the previous render count. A mismatch triggers an immediate error from `ReactCurrentDispatcher`. The `eslint-plugin-react-hooks` catches most violations statically before runtime, which is why it ships as standard in any React project setup. ### Common mistakes **Hooks inside a condition** ```tsx if (isLoggedIn) { const [user, setUser] = useState(null); // shifts all subsequent slots } ``` Fix: call the Hook unconditionally first, then check `isLoggedIn` below. **Hooks inside an event handler** ```tsx function handleClick() { const [local, setLocal] = useState(0); // plain JS function, not tracked by React } ``` Fix: move state to the component level. Use `useCallback` if the handler needs memoization. **Hooks inside a loop** ```tsx items.map(item => { const [active, setActive] = useState(false); // new instances every render return <li>{active ? 'on' : 'off'}</li>; }); ``` Fix: one `useState` at the top, store values in an object keyed by item id. **Custom Hook without `use` prefix** ```tsx function myFetchLogic() { const data = useSWR('/api/data'); // ESLint won't flag violations inside this } ``` Fix: rename to `useMyFetchLogic`. The `use` prefix tells both React and ESLint this is a Hook. **Hooks outside a component** ```tsx const globalCount = useState(0); // module scope, not tracked ``` Fix: move inside a component or into a custom Hook. ### Real-world usage - React core: `useState` and `useEffect` in every functional component since React 16.8 - Next.js: `useSWR` from Vercel for data fetching in app router pages - Redux Toolkit: `useSelector` and `useDispatch` in any connected component - TanStack Query: `useQuery` and `useMutation`, over 2 million weekly downloads - Zustand: `useStore` for lightweight global state in Vercel-built apps In my experience reviewing code, the "hooks inside a `.map()`" mistake shows up most often in teams migrating from class components. The fix is always the same: one Hook, one array outside the loop. ### Follow-up questions **Q:** Why does React use call order instead of Hook names or IDs? **A:** Names aren't unique. Two `useState` calls in the same component would collide. Call position gives a unique, stable slot per Hook with no extra metadata needed. **Q:** Can you call a Hook after an early return? **A:** No. A Hook after a `return` gets skipped on that render, breaking the slot count. All Hooks must run before any conditional exits. **Q:** What is a custom Hook and when should you create one? **A:** A function starting with `use` that calls other Hooks inside. Create one when the same stateful logic repeats across two or more components. **Q:** How does `eslint-plugin-react-hooks` know where Hooks are called? **A:** It parses the AST and flags any function starting with `use` called inside a condition, loop, or non-component function. It also enforces the `use` naming convention for custom Hooks. **Q (senior):** What exactly happens in React's fiber when Hook order changes between renders? **A:** Each fiber stores a `memoizedState` linked list. On update, React reads nodes in sequence. A missing node means the current node holds data from the previous node. All subsequent reads are offset by one slot. React detects the count mismatch via `ReactCurrentDispatcher` and throws before returning inconsistent state to the component. ## Examples ### Basic: Counter with two state slots ```tsx function Counter() { const [count, setCount] = useState(0); // Slot 1 const [label, setLabel] = useState('clicks'); // Slot 2 - always called return ( <div> <p>{count} {label}</p> <button onClick={() => setCount(c => c + 1)}>+1</button> </div> ); } // Output: stable count and label across all renders. Slots never shift. ``` Both Hooks run on every render. React restores `count` to slot 1 and `label` to slot 2 every time without confusion. ### Intermediate: Todo list with filtering ```tsx function TodoList({ todos }: { todos: { id: number; text: string; done: boolean }[] }) { const [filter, setFilter] = useState<'all' | 'active' | 'done'>('all'); // Slot 1 const visibleTodos = useMemo( () => filter === 'all' ? todos : todos.filter(t => filter === 'done' ? t.done : !t.done), [todos, filter] ); // Slot 2 return ( <div> <select onChange={e => setFilter(e.target.value as any)} value={filter}> <option value="all">all</option> <option value="active">active</option> <option value="done">done</option> </select> <ul>{visibleTodos.map(t => <li key={t.id}>{t.text}</li>)}</ul> </div> ); } // Output: filter state survives re-renders. Memo skips recalculation when inputs haven't changed. ``` `useState` at slot 1, `useMemo` at slot 2, no conditions around either call. The filtering logic lives inside `useMemo`, not wrapped around the Hook itself. ### Advanced: Early return after Hooks ```tsx function Profile({ userId }: { userId: string | null }) { const [profile, setProfile] = useState(null); // Slot 1 - runs every render const [posts, setPosts] = useState([]); // Slot 2 - runs every render if (!userId) return <div>No user selected</div>; // early return is fine here // fetch and render logic only when userId exists return <div>Profile for {userId}</div>; } // Output: slot count never changes because both useState calls complete before the conditional exit. ``` Both `useState` calls run before the `if (!userId)` check, so React always counts two Hook slots. Flip the order and you break the rule immediately.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.