Invalid Hook Call: Fix React Hooks Fast
Invalid hook call means two React copies, a broken Rules of Hooks pattern, or a version mismatch.
20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.
- ✓React hooks basics: useState and useEffect in functions
- ✓npm and bundler comfort for reading dependency trees
- ✓ESLint available in the project with JSX support
- Invalid hook call means React's dispatcher is missing: two React copies loaded, a version mismatch, or hooks called where they can't run
- Run npm ls react and npm ls react-dom, then dedupe so the app and every linked library resolve one React
- Call hooks only at the top level of function components or custom hooks — never in conditions, loops, or nested helpers
- Align react and react-dom versions and enforce eslint-plugin-react-hooks so bad patterns fail before they ship
Think of hooks as numbered coat-check tickets. React hands out ticket 1, 2, 3 in the same order on every visit, and uses that order to find your coat (your state). If you sometimes skip the counter (a condition), visit twice (a loop), or walk into a different building (a second copy of React), your tickets stop matching and the clerk refuses service. The fix is simple: always visit the same counter in the same order, in the same building, every single time.
Your app compiles, the page loads, and then the console throws Invalid hook call with three possible causes listed underneath. The stack points at a useState line that looks completely innocent — it's inside a component, it's spelled right, and it worked in another file. So you stare at the one line that's fine while the real fault sits somewhere you'd never think to look: the dependency tree, a conditional two lines above, or a library shipping its own React.
That confusion is by design. React can't tell which of its three rules you broke, so it lists all of them: mismatched versions, broken Rules of Hooks, or duplicate React copies. Each cause produces the identical message, yet each needs a different fix. Guessing wastes hours because deduping won't save a conditional hook, and moving a hook won't save two Reacts.
This guide gives you a diagnosis order that ends the guessing. You'll check for duplicate React with npm ls, spot conditional and nested hook calls, catch hooks smuggled into class components, align renderer versions, and lock the rules with ESLint. By the end, you'll read this error as a checklist instead of a riddle — and you'll know which single command proves each cause in seconds.
Two React Copies: npm ls React Shows the Culprit
Duplicate React is the leading cause of this error and the hardest to see in source code, because every file looks right. It happens when your bundler resolves two React module instances: your app imports from the root node_modules copy while a linked library, monorepo package, or aliased path imports from a nested copy. Hooks carry an internal reference to their own copy's dispatcher, so a useState from copy A that renders under copy B finds no active dispatcher and throws. Same version numbers won't save you — the check is object identity, not semver.
Diagnosis takes one command: npm ls react prints the resolved tree, and any nested react beneath a library entry is your smoking gun. yarn list react and pnpm list react show the same shape in their ecosystems. Common producers include npm link during local library development, file: dependencies that bundle node_modules, and monorepos where the app and the package each installed React separately. Case-sensitive import paths on macOS versus Linux can also split one copy into two resolved modules that look identical in the editor.
The fix has two halves: remove the nested copy and prevent its return. Move react and react-dom to peerDependencies in every shared library, dedupe the tree, and add an npm overrides entry plus a bundler alias pinning react to the single root file. Then add a CI step asserting npm ls react shows exactly one version — the checkout outage in this article's incident section would have failed that gate before deploy. When the error names clean code in one surface only, think copies first and prove it with the tree before touching components.
Hooks in Conditionals, Loops, and Nested Functions
React matches hook state by call order, not by name. On every render of a component, the first hook call gets slot one, the second gets slot two, and so on — the same sequence must run each time. A useState inside an if block, a useEffect inside a for loop, or a hook behind an early return shifts every slot below it whenever the condition flips. React detects the mismatch and throws Invalid hook call rather than handing your state to the wrong variable, which would corrupt data silently.
Nested functions are the same bug in disguise. Calling a hook inside an event handler, a helper defined within the component, or a promise callback runs it outside the render pass, where no dispatcher is active. The code reads as if it belongs to the component because it sits inside the same file, but execution timing decides — hooks must run synchronously during the component body or inside a custom hook called from that body. Custom hooks are the legal escape hatch: a use-prefixed function whose own hook calls run unconditionally at its top level preserves the order contract while sharing logic.
Refactor toward unconditional top-level calls and push branching into values. Compute the condition first, then call every hook with arguments derived from it, instead of wrapping the calls themselves. Lint with react-hooks/rules-of-hooks set to error so violations break the build instead of the checkout page. Once the order is stable across renders, this whole class of the error disappears — and the components get easier to read because every hook sits in one predictable block at the top.
Calling Hooks from Class Components by Mistake
Hooks only work inside function components and custom hooks — never inside classes. A class extending React.Component manages state through this.state and this.setState, with no dispatcher wired for useState or useEffect. When a hook call lands in a class method, a render helper on the class, or a higher-order wrapper that passes class context through, React finds no hook dispatcher for that execution and throws the same Invalid hook call message. Mixed codebases hit this during migrations, when a copied function-component snippet keeps its hooks inside a class file.
The confusion usually enters through shared helpers. A utility file exporting a function that calls useState looks callable from anywhere, and a class method importing it will crash at runtime with no lint error unless the plugin is configured. The same happens with decorators or mixins that wrap class render output and smuggle hook calls into class-owned execution. The stack trace names the hook line inside the helper, which sends reviewers to inspect the helper instead of its illegal caller.
Convert the boundary instead of patching the call. Turn the class into a function component when the file is ready, or extract the hook logic into a custom useX hook consumed by function components while the class keeps setState and lifecycle methods. Name custom hooks with the use prefix so the linter tracks them, and keep plain utilities hook-free so classes can safely import them. During migrations, grep for hook imports inside class files regularly — each one is a crash waiting for its first render.
React vs Renderer Version Mismatch
Hooks are a contract between react and its renderer — react-dom on the web, react-native on mobile, or a test renderer in suites. Both sides must come from the same major version because they share internal dispatcher shapes that change between majors. When package.json drifts to react 19 with react-dom 18 (or a stale lockfile pins one side), hooks initialize against internals the renderer doesn't provide, and the first hook call in the app throws. The message lists version mismatch second for a reason: upgrades cause it constantly.
Drift creeps in through partial upgrades and loose ranges. A caret range that floats react forward while the lockfile holds react-dom back, a types package updated without its runtime pair, or a template that scaffolds mismatched majors all produce the same crash on first render. Duplicate copies complicate the picture: npm ls may show one react but two renderers, or matching majors with a rogue nested minor that still resolves separately. Read the full tree for both packages, not just the top-level versions.
Align deliberately and rebuild cleanly. Pin react, react-dom, and @types/react to one major line, reinstall from scratch to collapse lockfile drift, and clear build caches so no stale bundle survives. Verify with npm ls react react-dom showing a single agreed version before opening component code. Teams that upgrade on a schedule — both packages in one PR with the test suite green — stop seeing this cause entirely, which leaves the weirder duplicates as the only remaining suspect when the message appears.
Enforcing Rules of Hooks with ESLint
Human review can't reliably catch hook-order bugs — the violations hide in conditions that look sensible and helpers that look local. The eslint-plugin-react-hooks package automates the two Rules of Hooks: call hooks only at the top level, and only from React functions or custom hooks. Its rules-of-hooks rule flags conditionals, loops, nested functions, and class usage at authorship time, while exhaustive-deps warns when effect dependencies drift. Teams that set the first to error convert runtime crashes into red squiggles.
Setup is short and pays off immediately. Install the plugin, extend its recommended config or enable the two rules directly, and run the linter over JavaScript and TypeScript sources including linked packages. Wire the same command into CI so violations fail pull requests instead of reaching staging. The exhaustive-deps warnings deserve attention too: silencing them with disables often masks the effect-ordering confusion that produces the next invalid call, so fix the dependency list instead of muting it.
Treat lint output as the first responder for this error, not an optional chore. When Invalid hook call appears, run the linter before restructuring components — a flagged line adjacent to the named hook usually is the cause. Keep the plugin version aligned with your React major, since rule updates track new hook APIs. Combined with the single-React CI gate, these two checks close every common path to this message: one proves a single copy, the other proves legal call order, and together they leave version skew as the only suspect.
Tracing the Offending Hook Call in Minutes
A fixed elimination order turns this error from a riddle into a checklist. Start with duplicates because they're fastest to prove: npm ls react and npm ls react-dom take seconds and explain the most cases outright. If the tree shows one copy, move to call order — open the file from the stack trace and scan upward for conditions, loops, early returns above hooks, and nested helpers. If the file is clean, widen to its imports: a custom hook or utility it calls may hold the illegal pattern one layer down.
Read the stack literally. The top frame names the hook that found no dispatcher; the frames below name the component or helper that executed it. A crash in a shared component used by one page only suggests that page's import resolved a second React. A crash on first paint after an upgrade suggests version skew. A crash after interaction or only for certain users suggests conditional order — logged-out, empty, or loading states that flip a condition the happy path never flips. Match the trigger shape to the cause class before changing code.
Close the loop with a reproduction you can keep. Strip the suspect to a minimal component rendering the hook unconditionally, confirm it passes, then reintroduce conditions until it breaks — the last addition is the fault. Keep that minimal case as a regression test plus the CI gates for duplicates and lint. Future occurrences then resolve in minutes: run the tree command, run the linter, read the trigger shape, and the checklist names the fix without a single guess.
Checkout Down After a Library Linked Its Own React Copy
- Identity beats version: two copies of the same React version still break hooks, so assert one resolved copy in CI.
- Libraries must peer-load React, never bundle it — dependencies entries for react are a release-night outage waiting.
- Blast radius hints at the cause: one surface crashing while others render points at a duplicated import, not a global upgrade.
| File | Command / Code | Purpose |
|---|---|---|
| io | { | Two React Copies |
| io | export function BrokenProfile({ user }) { | Hooks in Conditionals, Loops, and Nested Functions |
| io | export class BrokenCounter extends React.Component { | Calling Hooks from Class Components by Mistake |
| io | const pkg = require('./package.json'); | React vs Renderer Version Mismatch |
| io | module.exports = { | Enforcing Rules of Hooks with ESLint |
Key takeaways
Common mistakes to avoid
5 patternsEditing the named hook line instead of diagnosing the tree
Wrapping hooks in conditions to skip work for empty states
Bundling React inside a shared library's dependencies
Upgrading react without react-dom in the same PR
Silencing exhaustive-deps instead of fixing the dependency list
Interview Questions on This Topic
What are the two Rules of Hooks?
Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.
That's React.js. Mark it forged?
7 min read · try the examples if you haven't