The Error Scenario
Your app was working fine. You added a quick if (!userId) return at the top of a component โ sensible defensive code โ and now React throws:
Error: Rendered more hooks than during the previous render.
The whole app crashes. The component that rendered cleanly two minutes ago is now broken.
This is the exact pattern that triggers it:
function UserProfile({ userId }) {
if (!userId) {
return <p>No user selected.</p>;
}
// โ Hook is called AFTER an early return
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
return <div>{user?.name}</div>;
}
First render: userId is set, React runs both hooks. Second render: userId is undefined, the early return fires, and React sees zero hooks where it expected two. That mismatch is the error.
Why React Cares About Hook Order
React tracks hooks by position, not by name. Every render, it walks down a fixed-size internal list โ slot 0 is useState, slot 1 is useEffect, and so on. When you put a hook after a conditional return, the list length changes between renders. React can't tell which state belongs to which hook anymore.
This is the Rules of Hooks: call hooks at the top level of your component, unconditionally, every single render. No exceptions.
Quick Fix: Move All Hooks Before Any Return
Hoist every hook to the top of the function. Put conditional returns below them:
function UserProfile({ userId }) {
// โ
Hooks always run first
const [user, setUser] = useState(null);
useEffect(() => {
if (!userId) return; // guard goes inside the effect
fetchUser(userId).then(setUser);
}, [userId]);
// Early return comes AFTER hooks
if (!userId) {
return <p>No user selected.</p>;
}
return <div>{user?.name}</div>;
}
The guard logic moves inside the useEffect callback. The hook itself still runs every render โ React is happy.
Three Other Patterns That Break This Rule
Pattern 1: Hook inside an if-statement
// โ Wrong
function Toggle({ isLoggedIn }) {
if (isLoggedIn) {
const [count, setCount] = useState(0); // skipped when logged out
}
return <div />;
}
// โ
Correct
function Toggle({ isLoggedIn }) {
const [count, setCount] = useState(0); // always runs
return <div>{isLoggedIn ? count : null}</div>;
}
Pattern 2: Hook inside a loop
Loop length can change between renders โ same problem, different shape.
// โ Wrong
function ItemList({ items }) {
return items.map((item) => {
const [selected, setSelected] = useState(false);
return <Item key={item.id} selected={selected} />;
});
}
// โ
Correct โ extract to a child component
function ItemList({ items }) {
return items.map((item) => <Item key={item.id} />);
}
function Item({ id }) {
const [selected, setSelected] = useState(false); // top-level in its own component
return <div onClick={() => setSelected(!selected)}>{id}</div>;
}
Pattern 3: Conditional custom hook
// โ Wrong
function DataFetcher({ shouldFetch, url }) {
if (shouldFetch) {
const data = useFetch(url); // skipped sometimes
}
}
// โ
Correct โ pass the condition into the hook
function DataFetcher({ shouldFetch, url }) {
const data = useFetch(shouldFetch ? url : null);
}
Design custom hooks to accept a null or disabled value rather than wrapping the call in a condition. Most well-written hook libraries (SWR, React Query) already support this pattern.
Stop It at the Source: ESLint Plugin for Hooks
Install the official plugin and you'll catch this in your editor before the app even runs:
npm install eslint-plugin-react-hooks --save-dev
Wire it up in your ESLint config:
// .eslintrc.js
module.exports = {
plugins: ['react-hooks'],
rules: {
'react-hooks/rules-of-hooks': 'error', // flags conditional hooks
'react-hooks/exhaustive-deps': 'warn', // flags missing dependencies
},
};
Create React App and Vite's React template already bundle this plugin. Check that rules-of-hooks is set to 'error', not 'warn' โ a warning is easy to ignore under deadline pressure. With 'error', the linter refuses to let the violation slide.
Confirming the Fix Works
- Error gone โ no more crash in the browser console.
- ESLint clean โ run
npx eslint src/YourComponent.jsxand confirm zeroreact-hooks/rules-of-hooksviolations. - Test the edge case โ manually trigger the condition that caused the early return (pass
userId={undefined}, for example) and confirm the fallback UI renders without errors. - React DevTools โ open the component, toggle the prop a few times. Hook state should update cleanly without a crash.
Checklist When You See This Error
- Scan for any
returnstatement that appears above a hook call. - Check for hooks inside
if,switch, ternary expressions, orArray.map(). - Refactor conditional custom hooks to accept a disabled/null input instead.
- Extract list items that need their own state into separate child components.
- Enable
eslint-plugin-react-hooksโ catch this at write-time, not runtime.

