The component crashes on render and the console shows a numbered list:
Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.Often it is followed by TypeError: Cannot read properties of null (reading 'useState') (or useContext, useRef, whichever hook ran). React 18 prints the same list with a reactjs.org link; production builds show Minified React error #321. It means a hook ran at a moment when React was not rendering a function component, at least not from the point of view of the copy of React that the hook came from.
Quick fix checklist
- Is the hook inside a class component, an event handler, an effect callback or a
useMemocallback? Move it to the top level of a function component or custom hook. - Is it in a regular function whose name doesn’t start with
use, called from somewhere other than render? Rename it to a custom hook and call it at the top level, or stop using hooks inside it. - Run
npm ls react(orpnpm why react,yarn why react). More than one non-dedupedreactentry means two copies. - Check that
reactandreact-domhave the same version inpackage.jsonand the lockfile. - Working on a linked or
file:package? Make the app’s bundler resolve a singlereact(Viteresolve.dedupe, webpackresolve.alias).
Before you start
You should know what hooks are and that a custom hook is a function whose name starts with use and calls other hooks. Have a terminal open in the project root for npm ls. Details match React 19; React 18 differs only in the docs domain.
Why it happens
A hook call like useState(0) carries no information about its component. Instead, react reads a “current dispatcher” that react-dom sets right before calling your component and resets afterwards. During render, the dispatcher knows the component and its hook list; outside render it is empty or a placeholder that throws. The three numbered reasons map onto this design.
Breaking the Rules of Hooks. Call a hook while no component is rendering and there is no dispatcher. Typical places:
class Menu extends React.Component {
render() {
const [open, setOpen] = useState(false); // class components cannot use hooks
return null;
}
}
function SaveButton() {
function handleClick() {
const user = useContext(UserContext); // runs on click, long after render
save(user);
}
useEffect(() => {
const theme = useContext(ThemeContext); // runs after commit, not during render
}, []);
return <button onClick={handleClick}>Save</button>;
}Calling a hook inside a useMemo, useReducer or useState initializer callback gets a separate warning in development: Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks.
Hooks in conditions and loops are also against the rules, but they usually fail differently. React matches hooks by call order, so a conditional hook shows up as React has detected a change in the order of Hooks called by Cart, Rendered more hooks than during the previous render. or Rendered fewer hooks than expected. This may be caused by an accidental early return statement. The use API in React 19 is the one exception: it may be called conditionally.
More than one copy of React. Each copy of react has its own dispatcher variable, and react-dom only sets the one in the copy it imported. A component or library that imports a different copy finds an empty dispatcher, so even perfectly written code fails. That is where Cannot read properties of null comes from: the hook called a method on a null dispatcher.
Mismatched versions. If react and react-dom disagree about where the dispatcher lives, the hook cannot find it. Across the 18/19 boundary the failure is louder: React 19 renamed its shared internals, so loading react-dom@18 with react@19 crashes at import with Cannot read properties of undefined (reading 'ReactCurrentDispatcher'), and the reverse fails with (reading 'S'). Matching versions fixes both.
Step-by-step walkthrough
Step 1: Read the component stack
React 19 adds An error occurred in the <Menu> component. below the error. Open that component and find the hook from the TypeError line (reading 'useState'). If it fails inside a library’s hook (useForm, useQuery), suspect a second React copy and jump to Step 3.
Step 2: Check where the hook is called
Hooks belong at the top level of a function component or custom hook, before any early return, outside loops, conditions and callbacks. A less obvious violation is a helper that calls a hook but is also used from handlers:
// A helper that is secretly a hook
function formatPrice(cents) {
const locale = useContext(LocaleContext); // fine only if called during render...
return new Intl.NumberFormat(locale).format(cents / 100);
}
// ...so name it usePriceFormatter() and call it at the top levelThe rules-of-hooks lint rule from eslint-plugin-react-hooks catches these in the editor, but it relies on names: helpers that call hooks must start with use.
Step 3: Look for a second copy of React
From the project root:
npm ls reactA healthy tree has one real copy and every other entry marked deduped:
web-app@1.0.0
+-- @acme/ui-kit@1.4.0 -> ./../ui-kit
| `-- react@19.3.0
+-- react-dom@19.3.0
| `-- react@19.3.0 deduped
`-- react@19.3.0This one is not healthy. The react@19.3.0 under @acme/ui-kit is not deduped: the linked package has its own node_modules/react. Same version, different module instance, different dispatcher.
Step 4: Collapse to one copy
The cleanest fix depends on where the extra copy comes from:
- A published library listing
reactindependencies. It should be apeerDependency. Upgrade the library or report it; in the meantime npmoverrides(or pnpmoverrides, yarnresolutions) can force a single version. - A locally linked package (
npm link,file:dependencies, monorepo workspaces with their own installs). Tell the bundler to always resolve React from the app:
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
resolve: { dedupe: ['react', 'react-dom'] },
});With webpack, set resolve.alias entries for react and react-dom that point at the app’s own node_modules copies.
- Version drift. Align
reactandreact-dom(and@types/reactif present), then reinstall.
To prevent a repeat, libraries list React only in peerDependencies (plus devDependencies for their tests) and mark it external in their build; monorepos keep one React version at the root.
Worked scenario
A team develops @acme/ui-kit beside their app and installs it with npm install ../ui-kit. After adding a useToggle hook to the kit, the app’s menu crashes:
// ui-kit/index.js
import { useState } from 'react';
export function useToggle(initial = false) {
const [on, setOn] = useState(initial);
return [on, () => setOn((v) => !v)];
}
// web-app/src/Menu.jsx
import { useToggle } from '@acme/ui-kit';
export function Menu() {
const [open, toggle] = useToggle();
return <button onClick={toggle}>{open ? 'Close' : 'Open'}</button>;
}The console shows the invalid hook call list, then Cannot read properties of null (reading 'useState'). Menu follows every rule, so reason 2 is out. npm ls react prints the tree from Step 3: the kit’s devDependency installed ui-kit/node_modules/react, and because node_modules/@acme/ui-kit is a symlink, imports from the kit resolve React from the kit’s own folder. react-dom set the dispatcher on the app’s copy; useToggle read the kit’s copy.
The fix is resolve.dedupe: ['react', 'react-dom'] in the app’s Vite config, plus confirming that the kit’s package.json lists React as a peer:
{
"name": "@acme/ui-kit",
"peerDependencies": { "react": "^19.0.0" },
"devDependencies": { "react": "19.3.0" }
}After restarting the dev server (once with vite --force, since Vite caches pre-bundled dependencies), the menu toggles. Installed from the registry, the kit would have no nested React at all.
Common mistake
The most tempting wrong fix creates the problem in the first place. A linked package fails to build with Cannot find module 'react', so someone runs npm install react inside the package (or moves React from peerDependencies to dependencies). The import now resolves, but to a second copy, and every hook in the package becomes an invalid hook call. A package should never ship or resolve its own React at runtime; the app provides it.
The second mistake is deleting node_modules and reinstalling when two copies are the cause. If a linked package still has its own node_modules/react, a fresh install recreates exactly the same tree. Read npm ls react first, then fix the resolution.
Verify the behavior
Rerun npm ls react. After the dedupe fix the tree itself may look the same (the kit’s folder still exists on disk), so prove what the bundle actually uses. Temporarily re-export a hook from the kit and compare it in the app:
// ui-kit/index.js (temporary)
export { useState as kitUseState } from 'react';
// web-app/src/main.jsx (temporary)
import { useState } from 'react';
import { kitUseState } from '@acme/ui-kit';
console.log('single React copy:', useState === kitUseState);
// before the fix: single React copy: false
// after the fix: single React copy: trueThen reload: the invalid hook call list should be gone.
Interview exercise
A colleague’s component follows the Rules of Hooks perfectly, yet it throws “Invalid hook call” only when imported from a shared package in your monorepo. Explain why a correct component can fail, and how you would prove it.
Answer and reasoning
Hooks do not know their component; they read a module-level dispatcher that react-dom fills in while rendering each function component. If the shared package resolves react to a different physical copy, the hook reads that copy’s dispatcher, which is null, so React reports an invalid call even though the code is correct. To prove it, run npm ls react (or pnpm why react) and look for an entry that is not deduped, or compare useState from the app’s and the package’s react import with ===. The fix is structural: the package declares React as a peer dependency, and the app’s bundler dedupes or aliases react and react-dom so both resolve to one copy.
Continue learning
Practise with the React interview questions and the React MCQs. Custom hook contracts explains how to design hooks that are safe to share, and StrictMode double invocation covers another dev-only behaviour that confuses hook debugging. If your hook problem is an update loop rather than a crash, see Too many re-renders. Official references: Rules of Hooks, the Invalid Hook Call Warning guide and eslint-plugin-react-hooks on react.dev.