Initializing Enclave...

How to Fix 'Cannot read properties of undefined (reading useContext)' in a React Custom Hook

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


TL;DR

  • What broke: Your custom hook is invoking useContext but React itself resolves to undefined at call time — caused by a duplicate React instance, a broken import, or the hook being invoked outside the React component lifecycle.
  • How to fix it: Ensure a single React instance across your dependency tree, verify your import path, and guard the hook with a runtime invariant check.
  • Action: Use our Client-Side Sandbox below to auto-refactor this — paste your hook code and get a corrected diff without sending your code to any external server.

The Incident (What Does the Error Mean?)

Raw error output:

TypeError: Cannot read properties of undefined (reading 'useContext')
    at useMyCustomHook (useMyCustomHook.js:7:1)
    at MyComponent (MyComponent.jsx:12:1)
    at renderWithHooks (react-dom.development.js:14985:1)

React.useContext (or the named import useContext) is being dereferenced on an object that is undefined. This is a hard throw — React's error boundary cannot catch errors thrown during hook initialization before the render phase completes. The result is a complete unmount of the component subtree rooted at the component invoking the hook. In production builds, this surfaces as a blank screen with zero user feedback.


The Attack Vector / Blast Radius

This is not a subtle warning — it is a synchronous crash during render. The blast radius depends on where in the tree the hook lives:

  • Root-level hook: Entire application goes white. No fallback UI renders unless a top-level <ErrorBoundary> is in place (and even then, the boundary must be above the crashing component).
  • Shared layout hook: Every route using that layout crashes simultaneously.
  • Library hook (e.g., in a published internal package): All consuming apps fail on upgrade.

The four root causes, ranked by frequency in production incidents:

  1. Duplicate React instances — Your app bundle and a library (e.g., a local npm link'd package, a monorepo workspace, or a misconfigured Webpack resolve.alias) each ship their own copy of React. The library's useContext points at its own React object; the app's renderer owns a different one. They are incompatible.
  2. Named import from a CJS/ESM boundary mismatchimport { useContext } from 'react' resolves to undefined when the bundler incorrectly resolves the CJS default export and the named export is not re-exported.
  3. Hook called outside the React tree — The custom hook is invoked in a utility function, an event handler outside a component, a setTimeout callback, or a plain class method. React's dispatcher is null at that point.
  4. React version below 16.8useContext does not exist. The property read on the React object returns undefined.

How to Fix It (The Solution)

Basic Fix — Verify the Import and Add a Runtime Guard

The fastest triage step: confirm useContext is not undefined before your hook runs.

- import React from 'react';
- 
- function useMyCustomHook() {
-   const value = React.useContext(MyContext);
-   return value;
- }
+ import { useContext } from 'react';
+ 
+ function useMyCustomHook() {
+   if (typeof useContext !== 'function') {
+     throw new Error(
+       '[useMyCustomHook] useContext is undefined. ' +
+       'Ensure React 16.8+ is installed and there is no duplicate React instance.'
+     );
+   }
+   const value = useContext(MyContext);
+   return value;
+ }

Why this matters: The named import { useContext } fails loudly at module parse time if React is completely missing, rather than silently binding to undefined via the default import object.


Enterprise Best Practice — Eliminate the Duplicate React Instance

This is the fix that actually resolves the issue in 80% of monorepo and library-linking scenarios.

Step 1 — Diagnose duplicate instances:

# In your app root
node -e "console.log(require.resolve('react'))" 
# In your linked library root  
node -e "console.log(require.resolve('react'))"
# If the two paths differ, you have a duplicate.

Step 2 — Fix via Webpack resolve.alias (Create React App / Webpack 5):

// webpack.config.js or craco.config.js
 module.exports = {
   resolve: {
+    alias: {
+      react: path.resolve('./node_modules/react'),
+      'react-dom': path.resolve('./node_modules/react-dom'),
+    },
   },
 };

Step 3 — Fix via Vite:

// vite.config.ts
 export default defineConfig({
+  resolve: {
+    dedupe: ['react', 'react-dom'],
+  },
 });

Step 4 — Fix via package.json peerDependencies in your library (prevents the problem at the source):

// your-library/package.json
 {
-  "dependencies": {
-    "react": "^18.0.0"
-  }
+  "peerDependencies": {
+    "react": ">=16.8.0"
+  }
 }

Step 5 — Add a hook invariant using the invariant pattern:

+ import { useContext, createContext } from 'react';
+ 
+ const MyContext = createContext<MyContextType | undefined>(undefined);
+ 
  function useMyCustomHook(): MyContextType {
-   const ctx = useContext(MyContext);
-   return ctx;
+   const ctx = useContext(MyContext);
+   if (ctx === undefined) {
+     throw new Error(
+       'useMyCustomHook must be used within a <MyProvider>. ' +
+       'Wrap your component tree with the provider.'
+     );
+   }
+   return ctx;
  }

Initializing createContext with undefined as the default and throwing on undefined consumption is the canonical TypeScript-safe pattern — it makes missing providers a loud, traceable error rather than a silent null propagation.


💡 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

Do not rely on catching this in production. Gate it before merge.

1. ESLint react-hooks/rules-of-hooks (non-negotiable baseline):

npm install --save-dev eslint-plugin-react-hooks
// .eslintrc.json
{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

This rule statically detects hooks called outside components or other hooks.

2. Duplicate React instance check in CI (add to your package.json scripts):

# ci-checks/check-react-dedupe.sh
REACT_PATHS=$(find node_modules -name 'react' -type d -path '*/node_modules/react' | grep -v '.bin')
COUNT=$(echo "$REACT_PATHS" | wc -l)
if [ "$COUNT" -gt 1 ]; then
  echo "ERROR: Multiple React instances detected:"
  echo "$REACT_PATHS"
  exit 1
fi

Run this as a pre-build step in your GitHub Actions / GitLab CI pipeline.

3. @arethetypeswrong/cli for library authors — catches ESM/CJS export mismatches before publishing:

npx @arethetypeswrong/cli --pack .

4. Vitest / Jest component render smoke tests:

+ import { renderHook } from '@testing-library/react';
+ import { useMyCustomHook } from './useMyCustomHook';
+ 
+ test('throws if used outside provider', () => {
+   expect(() => renderHook(() => useMyCustomHook())).toThrow(
+     'useMyCustomHook must be used within a <MyProvider>'
+   );
+ });

A hook that throws outside its provider is correct behavior — test that it throws the right error, not that it silently returns undefined.

Related Diagnostics

"Part of the Syntax Utility Matrix."

View all 153 Syntax Tools →