Initializing Enclave...

How to Fix React Minified Error #185: Hooks Called After useState Initial Value

Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 5–15 mins


TL;DR

  • What broke: A React Hook (useState, useEffect, useRef, etc.) is being called after a conditional return or inside an if block, violating the Rules of Hooks. React tracks Hook calls by call order — break the order, break the component.
  • How to fix it: Move every Hook call to the top of the function component, unconditionally, before any if, switch, or return statement.
  • Fast path: Use our Client-Side Sandbox above to paste your component and auto-refactor the Hook ordering without leaking your code to a third-party server.

The Incident (What Does This Error Mean?)

Raw error in production bundle:

Error: Minified React error #185;
visit https://reactjs.org/docs/error-decoder.html?invariant=185

Decoded invariant #185:

Invalid hook call. Hooks can only be called inside of the body of a function component.

React uses a linked list internally to track Hook state between renders. Every render must call Hooks in the exact same order. The moment a conditional block, early return, or loop causes a Hook to be skipped or added mid-sequence, React's internal cursor desynchronizes. The result is an immediate, unrecoverable render crash — your entire component subtree unmounts and the user sees a blank screen or an error boundary fallback.


The Attack Vector / Blast Radius

This isn't a subtle bug. It is a hard crash. The blast radius:

  • User-facing: The component and all its children unmount instantly. If no <ErrorBoundary> wraps the tree, the entire React root goes blank.
  • SSR environments (Next.js, Remix): A server-side render will throw, potentially returning a 500 to the client or breaking hydration, causing a full-page white screen even after the JS bundle loads.
  • Silent regression risk: This error is often introduced when a developer adds an early auth guard (if (!user) return <Redirect/>) before existing Hook calls. It passes local dev if the condition is always false during testing, then detonates in production when a real unauthenticated user hits the page.
  • CI blind spot: Standard Jest/RTL tests frequently miss this because the test suite mocks auth state, keeping the condition false and the Hook order intact during the test run.

How to Fix It

Basic Fix — Move Hooks Above All Conditional Returns

The offending pattern is an early return placed before a Hook call:

 function UserProfile({ userId }) {
+  const [profile, setProfile] = useState(null);
+  const [loading, setLoading] = useState(true);
+
   if (!userId) {
     return <p>No user selected.</p>;
   }

-  // ❌ These Hooks are called AFTER a conditional return.
-  // React never reaches them when userId is falsy,
-  // breaking the Hook call count between renders.
-  const [profile, setProfile] = useState(null);
-  const [loading, setLoading] = useState(true);

   useEffect(() => {
     fetchProfile(userId).then(setProfile).finally(() => setLoading(false));
   }, [userId]);

   if (loading) return <Spinner />;
   return <div>{profile?.name}</div>;
 }

Rule: Every useState, useEffect, useContext, useRef, useMemo, useCallback, and custom Hook call must appear at the top of the function body, before any branching logic.


Enterprise Best Practice — Enforce with ESLint at Commit Time

Do not rely on code review to catch this. The eslint-plugin-react-hooks package enforces Hook ordering statically:

 // .eslintrc.json
 {
   "plugins": ["react-hooks"],
   "rules": {
-    // ❌ Missing — this is why it shipped to production
+    "react-hooks/rules-of-hooks": "error",
+    "react-hooks/exhaustive-deps": "warn"
   }
 }

With "react-hooks/rules-of-hooks": "error", the above broken component fails the lint step and never reaches CI build, let alone production.

For monorepos using Nx or Turborepo, enforce this in the shared eslint-base config so every app and library inherits it:

 // packages/eslint-config-base/index.js
 module.exports = {
   plugins: ['react-hooks'],
   rules: {
+    'react-hooks/rules-of-hooks': 'error',
+    'react-hooks/exhaustive-deps': 'warn',
   },
 };

💡 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

Three layers. All three. No exceptions.

Layer 1 — Pre-commit (fastest feedback loop)

 # .husky/pre-commit
+#!/bin/sh
+npx lint-staged

 # lint-staged.config.js
+module.exports = {
+  '**/*.{ts,tsx,js,jsx}': ['eslint --max-warnings=0 --rule \'react-hooks/rules-of-hooks: error\''],
+};

This blocks the commit locally before it ever hits the remote.

Layer 2 — CI Pipeline Gate (GitHub Actions / GitLab CI)

 # .github/workflows/ci.yml
 jobs:
   lint:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
       - uses: actions/setup-node@v4
         with:
           node-version: '20'
       - run: npm ci
+      - run: npx eslint . --ext .ts,.tsx,.js,.jsx --max-warnings 0

--max-warnings 0 ensures any Hook violation is a hard pipeline failure, not a warning that gets ignored.

Layer 3 — Static Analysis in PR Review (Sonar / CodeClimate)

Configure SonarQube or CodeClimate to import the ESLint report as a quality gate condition. Set the quality gate to fail the PR if react-hooks/rules-of-hooks violations are introduced. This gives reviewers a clear, automated signal without requiring React internals expertise on every team member.

Summary checklist:

Layer Tool Trigger Blocks Deploy?
Pre-commit husky + lint-staged git commit Yes (local)
CI Lint ESLint + GitHub Actions PR / push Yes
PR Gate SonarQube quality gate PR merge Yes
Runtime (dev only) React StrictMode Dev server Warning only

Related Diagnostics

"Part of the Syntax Utility Matrix."

View all 153 Syntax Tools →