Home › JavaScript › Invalid Hook Call: Fix React Hooks Fast
Intermediate 7 min · September 23, 2026

Invalid Hook Call: Fix React Hooks Fast

Invalid hook call means two React copies, a broken Rules of Hooks pattern, or a version mismatch.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.

Follow
✓ Production
production tested
September 25, 2026
last updated
1,950
articles · all by Naren
Before you start⏱ 12 min
  • ✓React hooks basics: useState and useEffect in functions
  • ✓npm and bundler comfort for reading dependency trees
  • ✓ESLint available in the project with JSX support
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • 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
✦ Definition~90s read
What is React Invalid Hook Call Fix?

Invalid hook call is React's way of saying hooks ran without a valid dispatcher — the internal channel that connects useState, useEffect, and friends to the component currently rendering. Hooks don't take the component as an argument; they rely on call order against the renderer that's active right now.

★
Think of hooks as numbered coat-check tickets.

When that channel is missing or the order shifts, React throws instead of returning mismatched state, because silent mismatch would corrupt every value below the break.

Three distinct faults collapse into this one message. Duplicate React copies are the most common in real apps: the bundler loads one React for your code and a second for a linked library or aliased path, so hooks from copy A run against copy B's dispatcher and find nothing.

Broken Rules of Hooks come next — a useState inside an if, a useEffect inside a loop, or a hook inside a nested helper or class method shifts the call order between renders, so the dispatcher can't match stored state. Version mismatch is the third: react and react-dom (or react-native's renderer) from different major versions wire up different internals, leaving hooks with no counterpart on the renderer side.

The professional response is elimination in a fixed order. Confirm a single React with npm ls react and your bundler's resolution, since duplicates explain the most cases and take seconds to check. Then audit call order — top level only, same sequence every render — with eslint-plugin-react-hooks enforcing it.

Finally align react, react-dom, and types to one version and rebuild. Each step rules out a whole class, so you converge on the fix instead of shuffling code and hoping the message moves.

Plain-English First

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.

io/thecodeforge/errors/reactDedupe.config.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Diagnose: show every resolved React in the tree (want exactly one)
// $ npm ls react
// $ npm ls react-dom

// package.json: force a single React for the whole tree
{
  "overrides": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  }
}

// webpack.config.js: alias every react import to the root copy
const path = require('path');
module.exports = {
  resolve: {
    alias: {
      react: path.resolve(__dirname, 'node_modules/react')
    }
  }
};
Try it live
📊 Production Insight
A UI package's dependencies entry shipped a nested React that crashed only checkout. The tree showed two identical versions — proof that identity, not semver, decides. Rule: fail CI when npm ls react lists more than one copy.
🎯 Key Takeaway
Two copies of React break hooks even at identical versions.
Prove it with npm ls react before editing components.
Pin one copy with overrides plus an alias, then gate it in CI.

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.

io/thecodeforge/errors/hookOrder.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { useEffect, useState } from 'react';

// BROKEN: hook order changes when user is null
export function BrokenProfile({ user }) {
  if (!user) return null; // early return skips hooks below on some renders
  const [tab, setTab] = useState('posts');
  return <button onClick={() => setTab('likes')}>{tab}</button>;
}

// FIXED: same hook order on every render
export function FixedProfile({ user }) {
  const [tab, setTab] = useState('posts');
  useEffect(() => {
    if (user) document.title = user.name;
  }, [user]);
  if (!user) return null;
  return <button onClick={() => setTab('likes')}>{tab}</button>;
}
Try it live
📊 Production Insight
An early return before two hooks crashed profiles only for logged-out visitors — the exact users nobody tested. Moving hooks above the return fixed every anonymous session. Lesson: test the empty and loading states, not just the happy path.
🎯 Key Takeaway
Hook order must be identical on every render — conditions break slots.
Hoist all hooks above returns and branch on values, not calls.
Enforce the rule with ESLint so it fails the build, not users.

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.

io/thecodeforge/errors/classToFunction.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { useState } from 'react';

// BROKEN: hook inside a class has no dispatcher
export class BrokenCounter extends React.Component {
  render() {
    const [count, setCount] = useState(0); // throws here
    return <button onClick={() => setCount(count + 1)}>{count}</button>;
  }
}

// FIXED: same UI as a function component
export function FixedCounter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Try it live
📊 Production Insight
A half-migrated settings page kept one class that imported a hook-based helper. It crashed only for admins opening that tab. The fix took ten minutes once the trace was read as caller fault, not helper fault.
🎯 Key Takeaway
Classes can't host hooks — use setState and lifecycles there.
Extract hook logic into use-prefixed custom hooks for functions.
Grep class files for hook imports during every migration.

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.

io/thecodeforge/errors/checkReactVersions.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const pkg = require('./package.json');

function major(v) {
  const m = String(v).match(/(\d+)/);
  return m ? m[1] : null;
}

const deps = { ...pkg.dependencies, ...pkg.devDependencies };
const pairs = ['react', 'react-dom', '@types/react'].map((n) => [n, deps[n]]);

let bad = false;
for (const [name, range] of pairs) {
  if (!range) { console.log('missing: ' + name); bad = true; }
}
if (major(deps.react) !== major(deps['react-dom'])) {
  console.error('mismatch: react ' + deps.react + ' vs react-dom ' + deps['react-dom']);
  bad = true;
}
if (bad) { process.exit(1); }
console.log('aligned on React major ' + major(deps.react));
Try it live
📊 Production Insight
A caret range floated react to 19 while the lockfile held react-dom at 18 through two deploys. The crash survived a rollback because the lockfile, not the code, owned the mismatch. Pinning both majors in one PR ended it.
🎯 Key Takeaway
React and its renderer must share one major version line.
Read npm ls for both packages — drift hides in lockfiles.
Upgrade both sides in a single PR with tests green.

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.

io/thecodeforge/errors/eslintHookRules.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// .eslintrc.js — enforce hook order everywhere, including TS sources
module.exports = {
  extends: ['react-app'],
  plugins: ['react-hooks'],
  rules: {
    'react-hooks/rules-of-hooks': 'error',
    'react-hooks/exhaustive-deps': 'warn'
  },
  overrides: [
    { files: ['**/*.ts', '**/*.tsx'], rules: { 'react-hooks/rules-of-hooks': 'error' } }
  ]
};
// $ npx eslint --ext .js,.jsx,.ts,.tsx src packages/ui/src
Try it live
💡Make the Hook Rules Fail the Build
Set react-hooks/rules-of-hooks to error in every config including linked packages, and run ESLint in CI. A flagged conditional hook is tomorrow's outage — fail the PR while the fix is a two-line hoist.
📊 Production Insight
A repo ran the hook plugin only on the app folder while the linked UI package held an illegal conditional hook. Extending lint to both trees flagged it in seconds. Lesson: lint every tree that ships hooks, not just the app.
🎯 Key Takeaway
Automate the Rules of Hooks — reviewers miss conditional calls.
Set rules-of-hooks to error and run it in CI over all trees.
Fix exhaustive-deps warnings instead of disabling them.

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.

📊 Production Insight
Support insisted checkout was broken for everyone, but logs showed only guests crashed — the logged-in path never flipped the condition hiding a hook. Trigger shape (who crashes) named the cause before any file was opened.
🎯 Key Takeaway
Eliminate in order: duplicates, then call order, then versions.
Read trigger shape — who crashes tells you which class it is.
Keep a minimal repro plus CI gates so the next one takes minutes.
● Production incidentPOST-MORTEMseverity: high

Checkout Down After a Library Linked Its Own React Copy

Symptom
After a routine deploy, the checkout page crashed instantly with Invalid hook call while product pages rendered fine. The trace pointed at a useState inside the shared cart drawer — code that hadn't changed in weeks. Rollback restored checkout, but the next release broke it again, which ruled out deployment luck and pointed at a dependency shift.
Assumption
The team blamed a React 19 upgrade because the crash followed a version bump PR. They pinned react back to 18 and rebuilt twice, but the error persisted on both versions. The version was a red herring — both trees still contained two React copies, so no single-version pin could reconcile hooks against one dispatcher.
Root cause
The shared UI package had moved react from peerDependencies into dependencies and started shipping its own nested copy. The storefront bundled the root react 18 for app code plus the nested react 18 inside the UI package, and the cart drawer imported hooks from the nested copy. Same version number, two module instances — the dispatcher check compares identity, not semver, so every hook in the drawer threw.
Fix
The library moved react and react-dom back to peerDependencies with a single supported range, and the app added an npm override plus a webpack alias forcing one React resolution. CI gained an npm ls react assertion and a duplicate-package check that fails the build on nested copies. Checkout rendered on the next deploy, and the dedupe check has caught two regressions since.
Key lesson
  • 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.
Production debug guideFive probes that isolate duplicates, rule breaks, and version skew in minutes.5 entries
Symptom · 01
The error names a hook line that looks correct in your component
→
Fix
Check for duplicates first: run npm ls react and npm ls react-dom and look for more than one resolved version or nested copies under node_modules. Confirm with npm dedupe, then enforce one copy using npm overrides or a webpack resolve.alias for react, and rebuild with npm run build before retesting.
Symptom · 02
The crash appears only on some renders or after an interaction
→
Fix
Audit call order around the named hook: search the file for useState or useEffect inside if blocks, loops, early returns before hooks, or nested helper functions. Hoist every hook to the component top level in identical order, extract conditional logic into values passed to hooks, and run npx eslint --ext .js,.jsx,.ts,.tsx src to confirm react-hooks/rules-of-hooks passes.
Symptom · 03
The trace leads into a class component or a plain utility file
→
Fix
Open the throwing file and check for class extends React.Component or a hook imported into a non-component helper. Move hook logic into a function component or a custom useX hook, keep classes on setState and lifecycle methods, and grep with grep -rn 'useState\|useEffect' src --include='*.js' to find hooks living outside function components.
Symptom · 04
The error started right after a version bump or a new install
→
Fix
Compare renderer versions: run npm ls react react-dom and read package.json directly to confirm react, react-dom, and @types/react share one major line. Align them with npm install react@18 react-dom@18 (matching your target), clear drift with rm -rf node_modules package-lock.json followed by npm install, and rebuild with npm run build before retesting.
Symptom · 05
You need a repeatable gate so this never ships again
→
Fix
Lock the rules in CI: install eslint-plugin-react-hooks, enable react-hooks/rules-of-hooks as error and exhaustive-deps as warn, and add a build step running npm ls react plus eslint over the app and linked packages. Test with npm test -- --watchAll=false so hook-order regressions fail the pipeline instead of the checkout page.
Invalid Hook Call Causes Compared
Root CauseHow to ConfirmFixPrevention
Two React copies resolvednpm ls react shows nested copies under a libraryPeer-load React; dedupe with overrides and aliasCI gate asserting exactly one resolved React
Hook in condition, loop, or nested fnESLint rules-of-hooks flags the file; crash varies by stateHoist hooks to top level; branch on valuesrules-of-hooks set to error in CI
Hook called from a class componentTrace leads into extends Component or class methodConvert to function or extract a useX custom hookGrep class files for hook imports on every migration
React vs renderer version skewnpm ls react react-dom disagree on major versionsPin both to one major; clean reinstall and rebuildUpgrade both packages in a single tested PR
⚙ Quick Reference
5 commands from this guide
FileCommand / CodePurpose
iothecodeforgeerrorsreactDedupe.config.js{Two React Copies
iothecodeforgeerrorshookOrder.jsxexport function BrokenProfile({ user }) {Hooks in Conditionals, Loops, and Nested Functions
iothecodeforgeerrorsclassToFunction.jsxexport class BrokenCounter extends React.Component {Calling Hooks from Class Components by Mistake
iothecodeforgeerrorscheckReactVersions.jsconst pkg = require('./package.json');React vs Renderer Version Mismatch
iothecodeforgeerrorseslintHookRules.jsmodule.exports = {Enforcing Rules of Hooks with ESLint

Key takeaways

1
Invalid hook call means no dispatcher
duplicates, rule breaks, or version skew.
2
Prove one React with npm ls react before rewriting any component code.
3
Keep hooks top-level and unconditional
branch on values, never on calls.
4
Classes can't host hooks; extract useX custom hooks for shared logic.
5
Pin react and react-dom to one major and upgrade them together.
6
Enforce rules-of-hooks in CI over every tree that ships hook code.

Common mistakes to avoid

5 patterns
×

Editing the named hook line instead of diagnosing the tree

Symptom
Hours spent rewriting a correct useState while duplicates persist.
Fix
Run npm ls react first on every occurrence — prove one copy before touching components.
×

Wrapping hooks in conditions to skip work for empty states

Symptom
Crashes only for logged-out or loading users the team never tested.
Fix
Call hooks unconditionally and branch inside effects or render output using derived values.
×

Bundling React inside a shared library's dependencies

Symptom
One surface crashes while others render; versions look identical.
Fix
Move react to peerDependencies, add overrides and alias, and gate duplicates in CI.
×

Upgrading react without react-dom in the same PR

Symptom
First-render crash right after a version bump; rollback seems not to help.
Fix
Pin both plus types to one major, reinstall cleanly, and rebuild before retesting.
×

Silencing exhaustive-deps instead of fixing the dependency list

Symptom
Effect staleness grows until hook order gets refactored into a crash.
Fix
Resolve each warning by correcting deps or splitting effects; keep disables near zero.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What are the two Rules of Hooks?
Q02JUNIOR
Why do two copies of React break hooks at the same version?
Q03SENIOR
How does a conditional hook corrupt state below it?
Q04SENIOR
What does eslint-plugin-react-hooks enforce for you?
Q05SENIOR
Checkout crashes but product pages render. How do you isolate it?
Q01 of 05JUNIOR

What are the two Rules of Hooks?

ANSWER
Call hooks only at the top level of React function components or custom hooks, and only in the same order every render. No conditions, loops, nested functions, or class components — that stable order is how React maps state slots.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Can I call hooks inside useEffect or event handlers?
02
Do custom hooks need the use prefix to work?
03
Why does the error survive a React version rollback?
04
How do monorepos trigger duplicate React?
05
Is an early return above hooks ever safe?
06
What's the fastest check when this error appears?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.

Follow
✓ Verified
production tested
September 25, 2026
last updated
1,950
articles · all by Naren
🔥

That's React.js. Mark it forged?

7 min read · try the examples if you haven't

←
Previous
Next.js Hydration Mismatch Fix
53 / 53 · React.js
Next
TS2322 Type Not Assignable Fix
→