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 bounds

React 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 link or a monorepo and each package resolves to a different React installation
  • A mismatch between react and react-dom versions

Check for duplicate React instances:

npm ls react

You should see only one version of react. If you see multiple, fix it by:

Deduplicating with npm:

npm dedupe

Forcing 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/react

After fixing, delete node_modules and reinstall:

rm -rf node_modules package-lock.json
npm install

6. 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-hooks

Add 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-dom

Both 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-dom

Or 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