How to Fix the Invalid Hook Call Error in React
Fix React invalid hook call errors by checking the stack trace, Rules of Hooks, duplicate React copies, and react-dom version mismatches.
The invalid hook call error has three common causes: a Rules of Hooks violation in your own code, more than one copy of React in the app, or mismatched versions of react and react-dom. The stack trace tells you which one to check first.
It often shows up when your own component code is fine. You npm link a local component library, add a dependency, or restructure a monorepo, and the error appears without telling you which of the three causes you have.
Key Takeaways
- If the failing hook call sits in your own component file, the problem is where the hook is called; if it sits inside
node_modules, the problem is almost always a second copy of React. - Run
npm ls react(orpnpm why react, oryarn why react); if the output resolves more than one version of React, duplicate copies are your cause and no change to component code will fix it. - A component library must declare React in
peerDependenciesand exclude it from its build output; if it bundles its own React, every consuming app gets two copies. - The
rules-of-hookslint rule catches misplaced hook calls before the code runs, but no linter can detect a duplicate React copy, because that failure lives in the installed dependency tree, not the source. - In production the error arrives as minified error #321, so decode it on React’s error decoder before guessing at the cause.
What Does the Invalid Hook Call Error Mean?
React throws this error whenever a hook runs outside the render of a function component, and the message itself enumerates the possibilities:
Invalid hook call. Hooks can only be called inside of the body of a function component.
This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
React’s invalid hook call warning page covers all three, plus a catch-all section for the rarer cases. The rest of this article checks them in the order that matches how the error usually presents.
Read the Stack Trace First
Before touching any config, answer one question from the stack trace: does the frame that calls the hook sit in your own source files, or inside node_modules? If it points at your component file, you have a Rules of Hooks violation and the fix is in your code. If it points into a dependency that has worked before, you almost certainly have two copies of React, and nothing you edit in a component will change the outcome.
| Cause | How to confirm | Fix |
|---|---|---|
| Rules of Hooks violation | Stack trace points into your files | Move the hook to the top level of a component |
| Two copies of React | npm ls react resolves two versions | Dedupe the tree (see below) |
react/react-dom mismatch | npm ls react react-dom shows different versions | Install both together |
Fixing a React Invalid Hook Call in Your Own Code
Only two rules produce this error: hooks must be called during the render of a function component (or from a custom hook that a component calls), and they must sit at the top level of that component, not inside an if, a loop, or a nested function. A hook at module level, in an event handler, or in a plain helper function breaks the first rule; a hook inside a condition or a .map callback breaks the second.
The helper-function case is the one that surprises people, because the code looks reasonable:
// Wrong: buildLink is a plain function, not a component
export function buildLink() {
const { pathname } = useLocation(); // invalid hook call
return `https://example.com${pathname}`;
}
// Right: call the hook in a component, pass the value down
function Page() {
const { pathname } = useLocation();
return <a href={buildLink(pathname)}>Canonical</a>;
}
export function buildLink(pathname) {
return `https://example.com${pathname}`;
}
For the loop case, the fix is structural: extract a child component so each item owns its own state.
// Wrong: one hook call per array item
function List({ items }) {
return items.map((item) => {
const [open, setOpen] = useState(false); // invalid hook call
return <li key={item.id}>{item.name}</li>;
});
}
// Right: each row is a component with its own state
function Row({ item }) {
const [open, setOpen] = useState(false);
return <li onClick={() => setOpen(!open)}>{item.name}</li>;
}
function List({ items }) {
return items.map((item) => <Row key={item.id} item={item} />);
}
Why Do Two Copies of React Break Hooks?
Hooks only work when your app and react-dom both load the same react module. If each one gets its own copy, React throws this error even though every hook call in your code sits exactly where it should. Confirm it before anything else:
npm ls react # npm
pnpm why react # pnpm
yarn why react # yarn
pnpm why and yarn why work backwards from a package to whatever pulled it in, so you can see exactly which dependency drags in the second copy. Two situations account for most duplicates:
A linked local package. A library linked with npm link or pnpm link resolves React from its own node_modules, not yours, which is why the error often appears the moment you link a component library that worked fine when installed normally. The React docs cover the npm link case, where the fix is to point the library at the React already installed in the app. In Vite projects, list the packages in resolve.dedupe and Vite pins each of them to a single copy taken from the project root:
// vite.config.js
export default {
resolve: { dedupe: ['react', 'react-dom'] },
}
A library that ships React. If a package declares react as a regular dependency, or bundles it into its build output, every consumer gets two copies. The library-side fix is to declare React in peerDependencies with the range it supports and mark it as external in the build. The app-side workaround forces one resolution, and the field name depends on your package manager. npm reads overrides, where $react means “the same version I declare for react myself”:
{ "overrides": { "react": "$react", "react-dom": "$react-dom" } }
yarn reads resolutions instead, with a plain version as the value. Don’t put both fields in one file; each manager ignores the other’s key.
Mismatched react and react-dom
react and react-dom ship as a pair, so check both and install them in one command. Run npm ls react react-dom; if the two versions differ, reinstall them together (npm install react react-dom) so they resolve to the same release. This is the quickest cause to rule out, and ruling it out early keeps you from chasing phantom code bugs.
How Do You Catch It Earlier with a Linter?
The eslint-plugin-react-hooks package flags every code-shaped cause of this error at edit time. With ESLint’s flat config:
// eslint.config.js
import reactHooks from 'eslint-plugin-react-hooks';
import { defineConfig } from 'eslint/config';
export default defineConfig([reactHooks.configs.flat.recommended]);
On ESLint versions below 9.0.0, the legacy form is "extends": ["plugin:react-hooks/recommended"]. Next.js projects already get these rules through eslint-config-next. The rules-of-hooks rule catches conditional and misplaced hook calls before the code ever runs, but no linter can detect a duplicate copy of React or a version mismatch; those failures exist only in the installed dependency tree, so they surface only at runtime.
The Production Form: Minified Error #321
In a production build this error arrives as a minified error code rather than the full message, so decode the code before assuming which problem you have. React error #321 expands to the invalid hook call text; confirming that first stops you from debugging the wrong invariant. The minified stack rarely names the component that threw, which makes the duplicate-copy case especially hard to trace in production. A session replay tool such as OpenReplay, which captures the console error alongside the route and the interaction that preceded it, shows which component tree was mounting at the moment of the throw, and that usually points at the lazy-loaded chunk or third-party widget that carried in the second React copy.
Start with the Stack Trace
Treat the error as a routing problem, not a mystery: the stack trace sends you either into your own component (fix the hook’s placement) or into the dependency tree (run npm ls react and dedupe). Start with that one command; it settles the most confusing of the three causes in seconds, and everything after it is a known fix.
FAQs
Does a custom hook need to start with 'use' to avoid the invalid hook call error?
No. The 'use' prefix never causes or prevents this runtime error, because React does not check hook names at runtime. The prefix matters for tooling: eslint-plugin-react-hooks relies on it to recognize hooks and enforce the Rules of Hooks, so a wrongly named custom hook silently escapes lint checks. Rename it with the prefix so violations get flagged at edit time instead of in the browser.
Can I call hooks inside a class component?
No. Hooks work only in function components and in custom hooks called from them, so calling useState or useContext inside a class method throws the invalid hook call error. To use a hook alongside a class you cannot rewrite, create a small function component that calls the hook and passes the result to the class component as props, or convert the class to a function component.
Can two copies of React ever run on the same page without errors?
Yes. Two apps on one page can each load their own React quite happily, for example when different teams ship them separately. The error turns up only when a component and the react-dom instance rendering it disagree about which react module they are using. Separate copies are fine on their own; they break as soon as they share a single render tree.
Does deleting node_modules and reinstalling fix duplicate React copies?
Only when the duplicate came from a stale or conflicting install state, since a fresh install lets the package manager dedupe the tree. If a dependency declares react as a regular dependency, bundles React into its build output, or you linked it locally with npm link, the second copy returns on every install. Those cases need an overrides or resolutions entry, a peerDependencies fix in the library, or bundler-level dedupe.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k