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
useContextbutReactitself resolves toundefinedat 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:
- Duplicate React instances — Your app bundle and a library (e.g., a local
npm link'd package, a monorepo workspace, or a misconfigured Webpackresolve.alias) each ship their own copy of React. The library'suseContextpoints at its own React object; the app's renderer owns a different one. They are incompatible. - Named import from a CJS/ESM boundary mismatch —
import { useContext } from 'react'resolves toundefinedwhen the bundler incorrectly resolves the CJS default export and the named export is not re-exported. - Hook called outside the React tree — The custom hook is invoked in a utility function, an event handler outside a component, a
setTimeoutcallback, or a plain class method. React's dispatcher isnullat that point. - React version below 16.8 —
useContextdoes not exist. The property read on the React object returnsundefined.
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.