How to Fix 'React Hook is Called Conditionally' Error: Rules of Hooks Violation Debugging Guide
Bug Severity: CRITICAL | UX/User Impact: SEVERE — component crashes or renders stale/corrupt state | Time to Fix: 5–15 mins
TL;DR
- What broke: A React Hook (
useState,useEffect, customuseX, etc.) is being invoked inside anifblock, a&&short-circuit, a loop, or a nested function — React's reconciler loses track of hook call order across renders. - How to fix it: Move every hook call unconditionally to the top level of the component. Push the conditional logic inside the hook's body or after the hook's return value.
- Fast path: Use our Client-Side Sandbox above to paste your component and auto-refactor the conditional hook violation without leaking your source code.
The Incident (What Does the Error Mean?)
Raw error thrown by React:
Warning: React Hook "useEffect" is called conditionally.
React Hooks must be called in the exact same order in every component render.
Did you accidentally call a React Hook after an early return?
In production builds (React 16–18), this warning escalates to a thrown Error that unmounts the component tree at that boundary. If no ErrorBoundary is present, the entire page goes blank.
What React is actually doing: React tracks hooks using a linked list keyed by call order, not by name or variable. Every render must traverse that list in the same sequence. The moment you skip a hook call (because an if evaluated to false), every subsequent hook in the list is reading the wrong slot — wrong state, wrong ref, wrong effect dependency. The component is now operating on corrupted internal state.
The Attack Vector / Blast Radius
This is not a style violation. This is a reconciler desync.
Scenario: Component renders with isLoggedIn = true → 4 hooks execute → state slots 0–3 are populated. Next render, isLoggedIn = false → hook #2 is skipped → hooks #3 and #4 now read from slots 2 and 3 instead of 3 and 4. They are consuming each other's state. The result is one or more of:
- Silent data corruption:
useStatereturns a value belonging to a different state variable. - Infinite render loop: A
useEffectwith a shifted dependency array fires on every render. - Full component unmount: React throws in strict mode or when the hook count changes between renders (adding/removing a hook conditionally changes the list length entirely).
- Blast radius in a large tree: If the offending component is high in the tree (a layout, a provider wrapper, a route component), the crash propagates down to every child. No
ErrorBoundarybelow it will catch it — the boundary must be above it.
How to Fix It
Basic Fix — Move the Hook Above the Condition
The conditional logic belongs inside or after the hook, never around it.
function UserProfile({ isLoggedIn }) {
- if (!isLoggedIn) {
- return <Redirect to="/login" />;
- }
-
- const [profile, setProfile] = useState(null); // ❌ Hook called after early return
- useEffect(() => {
- fetchProfile().then(setProfile);
- }, []);
+ const [profile, setProfile] = useState(null); // ✅ Always called first
+ useEffect(() => {
+ if (!isLoggedIn) return; // ✅ Guard lives inside the effect
+ fetchProfile().then(setProfile);
+ }, [isLoggedIn]);
+
+ if (!isLoggedIn) {
+ return <Redirect to="/login" />; // ✅ Early return is fine AFTER all hooks
+ }
return <div>{profile?.name}</div>;
}
Enterprise Best Practice — Extract to a Custom Hook with Internal Guards
For complex components, encapsulate the conditional behavior inside a dedicated custom hook. This keeps the component's top-level hook list flat and stable, and makes the guard logic testable in isolation.
-// ❌ Conditional hook call scattered in component body
function Dashboard({ featureFlags }) {
- if (featureFlags.analytics) {
- useAnalyticsTracker(); // Breaks Rules of Hooks
- }
return <MainView />;
}
+// ✅ Custom hook absorbs the condition internally
+function useConditionalAnalytics(enabled) {
+ useEffect(() => {
+ if (!enabled) return; // Guard inside — hook call order stays stable
+ initAnalyticsTracker();
+ return () => teardownAnalyticsTracker();
+ }, [enabled]);
+}
+
+function Dashboard({ featureFlags }) {
+ useConditionalAnalytics(featureFlags.analytics); // ✅ Always called
+ return <MainView />;
+}
Key rules enforced by this pattern:
- The component's hook call count is always identical across renders.
- The
enabledflag is a dependency, not a gate. - This pattern is fully compatible with React's upcoming compiler (React Forget) and concurrent mode.
💡 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 bug should never reach a pull request review. Automate the gate:
1. ESLint — eslint-plugin-react-hooks (Non-Negotiable)
This is the canonical linter rule. It catches conditional hooks statically at save time.
// .eslintrc.json
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
Set it to "error", not "warn" — warnings get ignored; errors fail the build.
2. Block Merges in CI (GitHub Actions)
# .github/workflows/lint.yml
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx eslint 'src/**/*.{ts,tsx}' --max-warnings=0
# --max-warnings=0 ensures ANY lint error (including hooks violations) fails the pipeline
3. Pre-commit Hook (Local Enforcement)
# Using Husky + lint-staged
npx husky add .husky/pre-commit "npx lint-staged"
# lint-staged.config.js
module.exports = {
'**/*.{ts,tsx}': ['eslint --max-warnings=0']
};
4. TypeScript Strict Mode
While TypeScript alone won't catch hook ordering, enabling "strict": true in tsconfig.json combined with typed custom hooks surfaces misuse patterns earlier in the IDE before the linter even runs.
The enforcement chain: IDE ESLint plugin → pre-commit Husky → CI lint step → PR merge block. Any one of these four layers will catch a conditional hook before it ships.