Fix: React Hook "useXxx" is called conditionally. React Hooks must be called in the exact same order in every component render.
The Error
You write a React component and hit one of these errors:
ESLint (eslint-plugin-react-hooks):
React Hook "useState" is called conditionally. React Hooks must be called in the
exact same order in every component render.React Hook "useEffect" is called in function "fetchData" that is neither a React
function component nor a custom React Hook function.React Hook "useState" cannot be called at the top level. React Hooks must be called
in a React function component or a custom React Hook function.React runtime error:
Invalid hook call. Hooks can only be called inside of the body of a function component.Rendered more hooks than during the previous render.All of these point to the same root cause: you broke the Rules of Hooks. React requires that hooks are called in the exact same order, every single render, with no exceptions.
Why This Happens
React tracks hooks by their call order, not by name. Internally, React stores hook state in an array. On every render, it walks through that array in order: the first useState call gets slot 0, the second gets slot 1, and so on.
If you skip a hook on one render — because it was inside an if block that didn’t execute, or after a return statement that fired early — the array slots get misaligned. React reads the wrong state for the wrong hook, and your component breaks in unpredictable ways.
Here’s a simplified view of what React does internally:
// First render: Second render (hook skipped):
// Slot 0: useState Slot 0: useState
// Slot 1: useEffect Slot 1: useMemo ← WRONG, expected useEffect
// Slot 2: useMemo Slot 2: ??? ← out of boundsReact either detects this mismatch and throws an error, or (worse) silently uses the wrong state. The ESLint plugin catches most of these issues at development time before they reach the runtime.
Fix
1. Hook Inside an if/else or Conditional Expression
This is the most common cause. You put a hook call inside a condition, so it only runs on some renders.
Broken code:
function UserProfile({ userId }) {
if (!userId) {
return <p>No user selected.</p>; // early return before hooks
}
// These hooks are skipped when userId is falsy
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
if (loading) return <p>Loading...</p>;
return <h1>{user?.name}</h1>;
}The early return before the hooks means React sees 0 hooks on one render and 3 hooks on another. When the hook count increases between renders, React throws “Rendered more hooks than during the previous render.” When it decreases, React detects the mismatch and throws a similar error.
Fix — move all hooks above any conditional returns:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!userId) {
setLoading(false);
return;
}
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
if (!userId) return <p>No user selected.</p>;
if (loading) return <p>Loading...</p>;
return <h1>{user?.name}</h1>;
}The hooks always run. The condition moves inside useEffect. Conditional returns happen after all hook calls.
2. Hook Inside a Loop
Hooks inside loops run a different number of times depending on the data. React can’t track them reliably.
Broken code:
function ItemList({ items }) {
const itemStates = [];
for (const item of items) {
// This hook runs a different number of times each render
const [expanded, setExpanded] = useState(false);
itemStates.push({ expanded, setExpanded });
}
return (
<ul>
{items.map((item, i) => (
<li key={item.id} onClick={() => itemStates[i].setExpanded(e => !e)}>
{item.name} {itemStates[i].expanded && <Details item={item} />}
</li>
))}
</ul>
);
}Fix — extract a component for each item:
function ItemList({ items }) {
return (
<ul>
{items.map(item => (
<Item key={item.id} item={item} />
))}
</ul>
);
}
function Item({ item }) {
const [expanded, setExpanded] = useState(false);
return (
<li onClick={() => setExpanded(e => !e)}>
{item.name} {expanded && <Details item={item} />}
</li>
);
}Each Item component has its own hook call that runs exactly once per render. React tracks each component’s hooks independently.
3. Hook Inside a Regular Function (Not a Component or Custom Hook)
React hooks can only be called in two places: inside a React function component (name starts with a capital letter) or inside a custom hook (name starts with use). Calling a hook inside a plain function breaks the rules.
Broken code:
// This function name starts with lowercase — React doesn't recognize it
function fetchUserData(userId) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
return user;
}
function UserProfile({ userId }) {
const user = fetchUserData(userId); // ESLint error on fetchUserData
return <h1>{user?.name}</h1>;
}ESLint reports: React Hook "useState" is called in function "fetchUserData" that is neither a React function component nor a custom React Hook function.
Fix — rename it to a custom hook (prefix with use):
function useUserData(userId) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
return user;
}
function UserProfile({ userId }) {
const user = useUserData(userId);
return <h1>{user?.name}</h1>;
}The use prefix is not just a convention — it tells both React and the ESLint plugin that this function follows hook rules and can contain hook calls.
4. Hook After a Conditional Return Statement
This is a variation of cause 1. Any return statement before your hooks means those hooks won’t run on every render.
Broken code:
function Dashboard({ isAdmin }) {
if (!isAdmin) {
return <p>Access denied.</p>;
}
// These only run when isAdmin is true
const [stats, setStats] = useState(null);
useEffect(() => {
fetch('/api/admin/stats').then(r => r.json()).then(setStats);
}, []);
return <AdminPanel stats={stats} />;
}Fix — hooks first, conditions after:
function Dashboard({ isAdmin }) {
const [stats, setStats] = useState(null);
useEffect(() => {
if (!isAdmin) return;
fetch('/api/admin/stats').then(r => r.json()).then(setStats);
}, [isAdmin]);
if (!isAdmin) {
return <p>Access denied.</p>;
}
return <AdminPanel stats={stats} />;
}If you’re concerned about the cost of running hooks when isAdmin is false: don’t be. An unused useState and a useEffect that returns early have negligible overhead. Correct behavior matters more than micro-optimization. If your component is stuck in an infinite render loop instead, see Fix: Too many re-renders.
5. Multiple React Versions in node_modules
If two copies of React end up in your bundle, hooks called with one copy can’t find the state managed by the other copy. You get this runtime error:
Invalid hook call. Hooks can only be called inside of the body of a function component.This happens when:
- A dependency bundles its own copy of React instead of using yours as a peer dependency
- You use
npm linkor a monorepo and each package resolves to a different React installation - A mismatch between
reactandreact-domversions
Check for duplicate React instances:
npm ls reactYou should see only one version of react. If you see multiple, fix it by:
Deduplicating with npm:
npm dedupeForcing a single version with npm overrides (in package.json):
{
"overrides": {
"react": "$react"
}
}This tells npm to use your top-level react version everywhere.
For Yarn users, use the resolutions field:
{
"resolutions": {
"react": "18.2.0"
}
}For npm link issues, link React from the linked package back to your app’s copy:
cd your-linked-package
npm link ../your-app/node_modules/reactAfter fixing, delete node_modules and reinstall:
rm -rf node_modules package-lock.json
npm install6. Class Component Trying to Use Hooks
Hooks only work in function components. They cannot be used in class components at all.
Broken code:
class UserProfile extends React.Component {
render() {
const [name, setName] = useState(''); // Invalid hook call
return <input value={name} onChange={e => setName(e.target.value)} />;
}
}Fix — convert to a function component:
function UserProfile() {
const [name, setName] = useState('');
return <input value={name} onChange={e => setName(e.target.value)} />;
}If you can’t convert the entire class component, extract the part that needs hooks into a function component and use it as a child:
function NameInput({ value, onChange }) {
const [localValue, setLocalValue] = useState(value);
useEffect(() => {
onChange(localValue);
}, [localValue, onChange]);
return <input value={localValue} onChange={e => setLocalValue(e.target.value)} />;
}
class UserProfile extends React.Component {
render() {
return <NameInput value={this.state.name} onChange={name => this.setState({ name })} />;
}
}Correct Patterns
Move Conditions Inside useEffect
Instead of conditionally calling useEffect, always call it and put the condition inside:
// Wrong
if (shouldFetch) {
useEffect(() => { fetchData(); }, []);
}
// Right
useEffect(() => {
if (shouldFetch) {
fetchData();
}
}, [shouldFetch]);Use Computed Values Instead of Conditional Hooks
Instead of conditionally calling useMemo, compute the value inline or always call the hook:
// Wrong
let displayName;
if (user) {
displayName = useMemo(() => `${user.first} ${user.last}`, [user]);
} else {
displayName = 'Guest';
}
// Right
const displayName = useMemo(() => {
if (!user) return 'Guest';
return `${user.first} ${user.last}`;
}, [user]);Conditional Rendering Without Conditional Hooks
If you want to render completely different UIs based on a condition, keep hooks at the top and branch in the JSX:
function Page({ view }) {
const [data, setData] = useState(null);
const [query, setQuery] = useState('');
useEffect(() => {
fetch(`/api/${view}`).then(r => r.json()).then(setData);
}, [view]);
// Branching happens after hooks
if (view === 'search') {
return <SearchView query={query} onQueryChange={setQuery} results={data} />;
}
return <ListView data={data} />;
}Or extract each branch into its own component with its own hooks:
function Page({ view }) {
if (view === 'search') return <SearchPage />;
return <ListPage />;
}
function SearchPage() {
const [query, setQuery] = useState('');
// ... hooks specific to search
}
function ListPage() {
const [data, setData] = useState(null);
// ... hooks specific to list
}This second pattern is often cleaner because each component only has the hooks it needs.
Still Not Working?
Set Up eslint-plugin-react-hooks
The eslint-plugin-react-hooks plugin catches these errors before you even run your code. If you’re not using it, set it up now.
Install it:
npm install --save-dev eslint-plugin-react-hooksAdd it to your ESLint config (.eslintrc.json):
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}If you’re using ESLint flat config (eslint.config.js):
import reactHooks from 'eslint-plugin-react-hooks';
export default [
{
plugins: { 'react-hooks': reactHooks },
rules: {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
},
},
];Note: Create React App, Next.js, and Vite’s React template include this plugin by default. If you ejected from CRA or have a custom config, verify it’s still active.
The rules-of-hooks rule is set to "error" intentionally. Never downgrade it to "warn" or "off" — hook order violations cause bugs that are extremely hard to debug at runtime. If ESLint itself is throwing parse errors on your code, see Fix: ESLint Parsing error: Unexpected token.
Check for Duplicate React Instances
If you get “Invalid hook call” at runtime but your code looks correct, run:
npm ls react
npm ls react-domBoth should show a single version. If you see two different versions or two separate installations, see Fix 5 above.
You can also verify at runtime by adding this to your app’s entry point:
import React from 'react';
import ReactDOM from 'react-dom';
console.log('React:', React.version);
console.log('ReactDOM:', ReactDOM.version);If the versions don’t match, or if a component library is bundling its own React, you have a duplicate instance problem.
Next.js and Bundler-Specific Issues
Next.js Server Components: In Next.js 13+ with the App Router, components in the app/ directory are Server Components by default. Server Components cannot use hooks. Add "use client" at the top of any file that needs useState, useEffect, or other hooks:
"use client";
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}Without "use client", Next.js treats the component as a Server Component and you get a hydration mismatch error:
You're importing a component that needs useState. It only works in a Client Component
but none of its parents are marked with "use client", so they're Server Components by default.Webpack resolve.alias: If your Webpack config has a custom resolve.alias for react, make sure it points to the correct path. A wrong alias can cause React to be loaded from an unexpected location, resulting in duplicate instances.
pnpm strict dependency isolation: pnpm hoists packages differently than npm. If a library can’t find your app’s copy of React, add it to .npmrc:
public-hoist-pattern[]=react
public-hoist-pattern[]=react-domOr use pnpm.overrides in package.json:
{
"pnpm": {
"overrides": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
}Verify the Component Is Actually a Function Component
If your component name starts with a lowercase letter, React treats it as a DOM element, not a component. Hooks won’t work:
// Wrong — lowercase name, React treats this as a DOM element
function myComponent() {
const [count, setCount] = useState(0); // Invalid hook call
return <div>{count}</div>;
}
// Right — capitalized name
function MyComponent() {
const [count, setCount] = useState(0);
return <div>{count}</div>;
}This also applies to arrow function components assigned to lowercase variables:
// Wrong
const widget = () => {
const [open, setOpen] = useState(false);
return <div>{open ? 'Open' : 'Closed'}</div>;
};
// Right
const Widget = () => {
const [open, setOpen] = useState(false);
return <div>{open ? 'Open' : 'Closed'}</div>;
};Related: Fix: TypeError: Cannot read properties of undefined
Related Articles
Fix: Too many re-renders. React limits the number of renders to prevent an infinite loop.
How to fix 'Too many re-renders' in React. Covers calling functions in JSX instead of passing references, setState in the render body, useEffect infinite loops, object/array dependency issues, and how to debug re-renders with React DevTools.
Fix: Next.js Image Optimization Errors – Invalid src, Missing Loader, or Unoptimized
How to fix Next.js Image component errors including 'Invalid src prop', 'hostname not configured', missing loader, and optimization failures in production.
Fix: React Can't Perform a State Update on an Unmounted Component
How to fix the React warning 'Can't perform a React state update on an unmounted component' caused by async operations, subscriptions, or timers.
Fix: React useEffect runs infinitely (infinite loop / maximum update depth exceeded)
How to fix useEffect infinite loops in React — covers missing dependency arrays, referential equality, useCallback, unconditional setState, data fetching cleanup, event listeners, useRef, previous value comparison, and the exhaustive-deps lint rule.