Fix: React Hook "useXxx" is called conditionally. React Hooks must be called in the exact same order in every component render.
Part of: React & Frontend Errors
Quick Answer
How to fix 'React Hook is called conditionally', 'Rendered more hooks than during the previous render', 'Invalid hook call', and other React Hooks order errors. Covers conditional hooks, hooks in loops, hooks after early returns, duplicate React versions, and ESLint setup.
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.
The decision to make hook order positional rather than name-based was deliberate. Dan Abramov’s announcement at React Conf 2018 explained the trade-off: positional ordering gives hooks a tiny, fast representation (just an array index) and makes custom hooks composable without any naming machinery. The cost is the Rules of Hooks. Every subsequent attempt to relax those rules has run into the same wall — once you change the contract, every existing hook breaks.
A Note on the React Hooks Timeline
Knowing when each constraint and tool landed helps explain why advice from different blog posts often conflicts.
React 16.8 (February 2019) introduced hooks. Before that, all state lived in class components, and functional components could not hold state. The release came with the original Rules of Hooks and eslint-plugin-react-hooks 1.0. If you read pre-2019 advice that talks about componentDidMount and setState, it predates the hooks model entirely.
React 17 (October 2020) did not change the hooks API but rewrote the event delegation system. This is when a class of “hooks work in development but break in production” issues caused by duplicate React copies became less common — but they did not disappear. If you still see Invalid hook call in 2026, the underlying cause is almost always still a duplicated React install.
React 18 (March 2022) introduced strict mode’s double-invoke behavior for development. In strict mode, React intentionally renders components twice and runs effects twice on mount to surface side-effect bugs. This causes a wave of “my fetch fires twice” reports that look like hooks bugs but are not. The double invocation only runs in development and is documented behavior. React 18 also introduced concurrent rendering, which means a render can be paused and discarded — making hook order discipline more important than ever.
React 19 (December 2024) added the use() hook. use() is unique among hooks in that it can be called conditionally and inside loops — it reads a Promise or Context with Suspense semantics. This is the first crack in the “no conditional hooks” rule, but it is the exception that proves the rule: every other hook still requires the same order on every render. Don’t read articles about use() as license to put useState inside an if.
The ESLint plugin has been refined throughout this timeline. In 2024 it gained better detection for hooks called inside async functions and for hooks that escape into event handlers. If you are using an old ESLint config, upgrading the plugin alone usually surfaces issues you have been carrying for years.
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.
Pro Tip: If your component has complex conditional logic, try extracting each branch into its own component. Each child component can safely use its own hooks without worrying about the parent’s conditional rendering path.
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.
The use() Exception in React 19
React 19 added one hook that breaks the conditional rule on purpose: use(). It reads a Promise or Context and integrates with Suspense, and it is explicitly allowed inside conditionals and loops:
function Comments({ commentsPromise }) {
// Allowed: use() is the only hook that can be conditional
const comments = use(commentsPromise);
return comments.map(c => <p key={c.id}>{c.text}</p>);
}use() is the only exception. Do not generalize from this to think that other hooks now follow the same rules — useState, useEffect, useMemo, useReducer, and every custom hook still must be called in the same order on every render. If a tutorial tells you otherwise, double-check the React version it targets.
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>;
};Hooks Called From Async Callbacks or Event Handlers
A subtler version of the rule violation is calling a hook from inside an async function, a setTimeout, or an event handler. ESLint catches some of these, but not all. The fix is the same: lift the hook to the top of the component and use state to drive whatever the async code needs:
// Wrong — useState fires only when the user clicks
function Form() {
return <button onClick={() => {
const [v, setV] = useState(''); // Invalid hook call
setV('clicked');
}}>Click</button>;
}
// Right — useState at the top, setter used in the handler
function Form() {
const [v, setV] = useState('');
return <button onClick={() => setV('clicked')}>Click {v}</button>;
}Hot Reload Confusion After Editing a Custom Hook
When you rename a custom hook (say from useThing to useThingNew) while the dev server is running, Vite’s HMR or Next.js Fast Refresh occasionally keeps the old hook in scope for one render before swapping in the new module. You see Rendered more hooks than during the previous render. and the issue disappears after a full page reload. If a hook error appears once and never again after a refresh, this is the cause, not your code. If it persists, your code is the cause.
Hook Used in a Library That Bundles Its Own React
A third-party UI library that bundles React internally (rare, but it happens with older builds and some Storybook addons) effectively creates a duplicate React instance. Your npm ls react shows a clean tree, but the bundled copy still triggers Invalid hook call. Inspect the library’s published files for an import { useState } from 'react' reference and confirm it points outward. If it bundles, ask the maintainer to externalize React or switch to a different library.
Related: Fix: TypeError: Cannot read properties of undefined
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
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: AutoAnimate Not Working — Transitions Not Playing, List Items Not Animating, or React State Changes Ignored
How to fix @formkit/auto-animate issues — parent ref setup, React useAutoAnimate hook, Vue directive, animation customization, disabling for specific elements, and framework integration.
Fix: Blurhash Not Working — Placeholder Not Rendering, Encoding Failing, or Colors Wrong
How to fix Blurhash image placeholder issues — encoding with Sharp, decoding in React, canvas rendering, Next.js image placeholders, CSS blur fallback, and performance optimization.
Fix: Docusaurus Not Working — Build Failing, Sidebar Not Showing, or Plugin Errors
How to fix Docusaurus issues — docs and blog configuration, sidebar generation, custom theme components, plugin setup, MDX compatibility, search integration, and deployment.