How to Fix React 'Invalid Hook Call' Error: Hooks Must Be Called Inside a Function Component
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 5–15 mins
TL;DR
- What broke: A React Hook (
useState,useEffect,useContext, etc.) was called outside the body of a function component — inside a class component, a nested helper function, an event handler, or a conditional block. - How to fix it: Move all Hook calls to the top level of a React function component. Audit your
node_modulesfor duplicatereactpackages usingnpm ls react. - Fast path: Use our Client-Side Sandbox above to auto-refactor this — paste the offending component and get corrected code without sending your source to a third-party server.
The Incident (What Does the Error Mean?)
Raw error output from the browser console:
Uncaught Error: 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
at Object.throwInvalidHookError (react-dom.development.js:14906)
at useState (react.development.js:1497)
at fetchUserData (UserService.js:12)
at handleClick (UserCard.jsx:34)
Immediate consequence: The component tree fails to mount entirely. React's internal fiber reconciler detects a Hook call outside the expected call stack position and hard-throws — there is no graceful degradation. Every user hitting that route sees a blank screen or an error boundary fallback. If no error boundary is set, the entire application unmounts.
The Attack Vector / Blast Radius
This is not a security vulnerability in the traditional CVE sense, but its blast radius in production is equivalent to a hard crash:
- Full render failure. React does not attempt partial rendering. One invalid hook call kills the entire subtree below the nearest error boundary — or the entire app if none exists.
- Silent CI pass. Unit tests mocking React often don't execute the real reconciler, so this error routinely ships through a green pipeline and detonates in production on first render.
- Duplicate React instance cascade. The most insidious variant: a shared component library (
my-design-system) bundles its own copy ofreactinstead of declaring it as apeerDependency. Every Hook call in that library hits a different React instance than the host app. The reconciler's internal state counter is misaligned — Hook call N in the library maps to the wrong state slot in the host. This corrupts state silently before eventually throwing, meaning you may have been shipping bad state for weeks before the hard crash surfaces. - Class component contamination. Developers migrating legacy codebases often call
useStateinside a class component'srender()method or lifecycle. React's dispatcher is not initialized for class components — the call hits a null dispatcher and throws immediately.
The three root causes, ranked by frequency in production incidents:
| Rank | Root Cause | Detection Difficulty |
|---|---|---|
| 1 | Hook called in a nested/helper function, not at top level of component | Low — visible in stack trace |
| 2 | Hook called inside a class component | Low — obvious once you look |
| 3 | Duplicate react package in node_modules |
High — requires dependency audit |
How to Fix It (The Solution)
Root Cause 1 — Hook Called in a Nested Function or Event Handler
Basic Fix: Hoist the Hook call to the top level of the function component.
// UserCard.jsx
- function UserCard({ userId }) {
- function handleClick() {
- // ❌ Hook called inside an event handler — illegal
- const [data, setData] = useState(null);
- fetchUser(userId).then(setData);
- }
-
- return <button onClick={handleClick}>Load</button>;
- }
+ function UserCard({ userId }) {
+ // ✅ Hook at top level of the function component
+ const [data, setData] = useState(null);
+
+ function handleClick() {
+ fetchUser(userId).then(setData);
+ }
+
+ return <button onClick={handleClick}>Load</button>;
+ }
Root Cause 2 — Hook Called Inside a Class Component
Basic Fix: Convert to a function component, or wrap the class component with a function component adapter.
- class UserProfile extends React.Component {
- render() {
- // ❌ Hooks are illegal in class components
- const [profile, setProfile] = useState(null);
- return <div>{profile?.name}</div>;
- }
- }
+ // ✅ Converted to function component
+ function UserProfile() {
+ const [profile, setProfile] = useState(null);
+
+ useEffect(() => {
+ fetchProfile().then(setProfile);
+ }, []);
+
+ return <div>{profile?.name}</div>;
+ }
Enterprise Best Practice — Adapter Pattern for Legacy Class Components:
If you cannot refactor the class component immediately (e.g., it owns complex lifecycle logic tied to shouldComponentUpdate), inject state via a Higher-Order Component wrapper:
- // Legacy class component consuming a hook directly — crashes
- class LegacyWidget extends React.Component {
- render() {
- const theme = useTheme(); // ❌
- return <div style={{ color: theme.primary }} />;
- }
- }
+ // ✅ HOC adapter bridges the hook into a class component via props
+ function withTheme(WrappedComponent) {
+ return function ThemedComponent(props) {
+ const theme = useTheme(); // ✅ called in function component body
+ return <WrappedComponent {...props} theme={theme} />;
+ };
+ }
+
+ class LegacyWidget extends React.Component {
+ render() {
+ const { theme } = this.props; // ✅ received via props, not hook
+ return <div style={{ color: theme.primary }} />;
+ }
+ }
+
+ export default withTheme(LegacyWidget);
Root Cause 3 — Duplicate React Instance (Most Dangerous)
Diagnosis first:
# Check for multiple React copies in the dependency tree
npm ls react
# or
yarn list react
# Expected output: ONE version at the root
# Danger output: [email protected] appearing under node_modules/my-design-system/node_modules/react
// my-design-system/package.json
"dependencies": {
- "react": "^18.2.0" // ❌ bundled as a hard dependency — creates duplicate instance
- "react-dom": "^18.2.0"
},
+ "peerDependencies": { // ✅ declared as peer — host app provides the single React instance
+ "react": ">=17.0.0",
+ "react-dom": ">=17.0.0"
+ },
+ "devDependencies": {
+ "react": "^18.2.0", // ✅ only for local development/testing of the library
+ "react-dom": "^18.2.0"
+ }
If you cannot modify the library (third-party), alias React in your bundler config:
// webpack.config.js
resolve: {
+ alias: {
+ // ✅ Force all modules to resolve to the same React instance
+ react: path.resolve('./node_modules/react'),
+ 'react-dom': path.resolve('./node_modules/react-dom'),
+ },
}
// vite.config.js
resolve: {
+ dedupe: ['react', 'react-dom'], // ✅ Vite equivalent
}
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
This class of error is 100% preventable before it reaches production. Wire all three of the following into your pipeline:
1. ESLint Rules of Hooks Plugin (Catches Root Causes 1 & 2 at Commit Time)
npm install --save-dev eslint-plugin-react-hooks
// .eslintrc.json
"plugins": [
+ "react-hooks"
],
"rules": {
+ "react-hooks/rules-of-hooks": "error", // ✅ Hard error — breaks CI
+ "react-hooks/exhaustive-deps": "warn" // ✅ Catches stale closure bugs
}
Add to your CI pipeline so a PR with an invalid hook call cannot be merged:
# .github/workflows/lint.yml
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx eslint 'src/**/*.{js,jsx,ts,tsx}' --max-warnings=0
2. Dependency Duplicate Audit (Catches Root Cause 3 in CI)
# Add to your CI pipeline as a gate
npx npm-why react
# or use a custom script:
node -e "
const lock = require('./package-lock.json');
const dups = Object.keys(lock.packages).filter(p => p.endsWith('/react') && p !== 'node_modules/react');
if (dups.length) { console.error('Duplicate React instances:', dups); process.exit(1); }
"
3. Bundle Analysis Gate (Catches Duplicate React in the Final Build)
# webpack-bundle-analyzer or source-map-explorer
npx source-map-explorer 'build/static/js/*.js' --only-mapped
# Visually inspect: if you see react appearing more than once in the treemap, you have a duplicate
For monorepos using Nx or Turborepo, enforce a single React version via peerDependencies constraints in your root package.json:
// package.json (monorepo root)
"resolutions": {
"react": "18.2.0",
"react-dom": "18.2.0"
}
Bottom line: eslint-plugin-react-hooks with "error" severity catches 80% of these in the editor before a single line is committed. The duplicate instance check in CI catches the remaining 20% that static analysis cannot see.