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

A hook such as `useState` ran when React was not rendering a function component, or it ran against a different copy of React than the one rendering the page. Check two things first: that the hook is called at the top level of a component or custom hook, and that `npm ls react` shows a single copy of React.

| Fact | Detail |
|---|---|
| Error | `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:` |
| In production builds | `Minified React error #321`, or `Cannot read properties of null (reading 'useState')` in Chrome when there are two copies of React |
| Where you see it | Browser console, the Next.js error overlay, test output |
| Usual cause | A hook called in an event handler, a class component or a plain function, or two copies of React in the bundle |
| Usual fix | Call hooks at the top of a component or custom hook, and dedupe React |
| BugTape catches it | Yes, automatically, when it is thrown in an event handler or reaches no error boundary of yours. One line in your boundary otherwise |

## What the error means

React keeps track of the component it is rendering in an internal object, the dispatcher. Every hook reads that object. When nothing is rendering, the dispatcher is a stub that throws this error. That is what happens when a hook is called from a click handler, a class component or a function that runs outside rendering.

The full message lists three reasons:

```text
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.
```

The duplicate-React case looks different. Each copy of `react` has its own dispatcher, and only the copy that `react-dom` uses is ever set. A hook imported from the other copy finds no dispatcher at all. In development React first prints the invalid hook call message with `console.error`, and then the code fails with a `TypeError`. Chrome and Node.js print it as `Cannot read properties of null (reading 'useState')` (or `useContext`, `useRef` and so on). In production only the `TypeError` appears.

## Common causes

1. **A hook called outside a component's render.** Inside an event handler, a `setTimeout`, a promise callback, a `useEffect` callback or a utility function.
2. **A hook in a class component.** Class components cannot use hooks.
3. **Two copies of React.** A linked package (`npm link`, a monorepo workspace) or a library that lists `react` under `dependencies` instead of `peerDependencies` brings its own copy.
4. **Mismatched `react` and `react-dom` versions.** Current React DOM checks this when it loads and throws `Incompatible React versions: The "react" and "react-dom" packages must have the exact same version.` instead. Older versions could end with the invalid hook call.
5. **A component called as a plain function from outside rendering.** Calling `Modal()` from a handler runs its hooks with no component rendering.

Breaking the order rules, such as a hook inside an `if` or after an early `return`, usually gives a different error: `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 fix is the same: call every hook at the top level, in the same order on every render.

## How to fix it

### Call hooks at the top of the component

Read the value with the hook while rendering, then use it in the handler.

```tsx
// Before: useNavigate runs on click, outside rendering
function SaveButton() {
  function handleClick() {
    const navigate = useNavigate();
    navigate('/done');
  }
  return <button onClick={handleClick}>Save</button>;
}

// After
function SaveButton() {
  const navigate = useNavigate();
  return <button onClick={() => navigate('/done')}>Save</button>;
}
```

The same applies to effects: call `useContext` in the component body and read the value inside `useEffect`.

### Turn utility functions that use hooks into custom hooks

A function that calls hooks must itself be a hook (its name starts with `use`) and be called from a component's body.

```tsx
// Before: a plain function called from a handler
function getUser() {
  return useContext(UserContext);
}

// After
function useUser() {
  return useContext(UserContext);
}

function Profile() {
  const user = useUser();
  return <p>{user.name}</p>;
}
```

The `eslint-plugin-react-hooks` rules report both of these mistakes in the editor.

### Use a function component instead of a class

Convert the class, or wrap the hook in a small function component and pass the value down as a prop.

```tsx
function WithTheme({ children }: { children: (theme: Theme) => React.ReactNode }) {
  const theme = useContext(ThemeContext);
  return <>{children(theme)}</>;
}

// In the class component's render
<WithTheme>{(theme) => <Panel theme={theme} />}</WithTheme>
```

### Find and remove a second copy of React

```bash
npm ls react react-dom
```

Every entry should resolve to the same version and be marked `deduped`. If a library brings its own copy:

- In your own library, move `react` and `react-dom` to `peerDependencies` (keep them in `devDependencies` for tests).
- In an app, force one version with `overrides` in `package.json` (npm), `resolutions` (Yarn) or `pnpm.overrides` (pnpm):

```json
{
  "overrides": {
    "react": "$react",
    "react-dom": "$react-dom"
  }
}
```

- With linked packages in Vite, make the bundler use the app's copy:

```ts
// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  resolve: { dedupe: ['react', 'react-dom'] },
});
```

For `npm link` the React docs suggest running `npm link ../myapp/node_modules/react` from the library folder, so the library uses the app's React.

### Install matching versions

```bash
npm install react@latest react-dom@latest
```

Keep `react`, `react-dom` and, if you use them, `@types/react` and `@types/react-dom` on the same major version.

## How to catch it in production with BugTape

Where the error goes depends on where the hook ran.

- **In an event handler, a timer or a promise callback.** Error boundaries do not catch these. The error is uncaught, the browser fires a window `error` or `unhandledrejection` event, and the BugTape browser SDK reports it automatically with the page URL, the stack and the clicks before it.
- **During render with no error boundary of yours**, such as a class component or a duplicate copy of React. React 19 passes the error to `reportError()`, which fires a window `error` event, and React 18 rethrows it as an uncaught error. BugTape reports it automatically. In the Next.js App Router, an error that only the built-in boundary catches also goes to `reportError()`.
- **During render inside your own error boundary**, including a Next.js `error.tsx`. React and Next.js then only call `console.error`. BugTape records console output into a report but does not start one from it. Add one line to the boundary:

```tsx
'use client';
import { useEffect } from 'react';

export default function Error({ error, retry }: { error: unknown; retry: () => void }) {
  useEffect(() => {
    reportError(error); // fires the window error event BugTape listens for
  }, [error]);
  return <button onClick={() => retry()}>Try again</button>;
}
```

The development-only `console.error` that comes before the duplicate-React `TypeError` does not start a report. The `TypeError` itself does, by the same rules as above. The SDK records nothing before `init()` runs, so an error during the very first render can come before BugTape starts.

Add the browser SDK once (see the [quickstart](/docs/quickstart/)):

```ts
import { init } from '@bugtape/sdk';

init({ apiKey: 'YOUR_PROJECT_KEY', release: 'YOUR_GIT_SHA' });
```

Repeats group into one issue with a count. Set `release` so that when you mark the issue fixed, a report from a newer release reopens it as a regression, which is useful for a duplicate copy of React that comes back with the next dependency update. Your coding agent can read the issue over MCP (`npx -y bugtape connect claude`, or `cursor` or `codex`) and propose the change. You review the pull request.

## FAQ

### What is React error 321?

It is the production code for `Invalid hook call. Hooks can only be called inside of the body of a function component.` The production message links to `react.dev/errors/321`. Run the app in development for the full text and the component stack.

### Why do I get "Cannot read properties of null (reading 'useState')"?

The hook was imported from a copy of React that is not rendering the page, so it finds no dispatcher. It usually means two copies of React are installed. Run `npm ls react` and dedupe as described above.

### Can I use hooks in a Next.js Server Component?

No. State and effect hooks such as `useState` and `useEffect` work only in Client Components. Add `'use client'` at the top of the file that uses them, or move the interactive part into its own Client Component and render that from the Server Component.

### Why does the error appear only after installing a library?

The library most likely brought its own copy of React, or requires a different React version than your app uses. `npm ls react` shows which package pulled in the extra copy. Updating the library, or forcing one version with `overrides`, usually fixes it.

## Sources

- [React error codes](https://github.com/facebook/react/blob/main/scripts/error-codes/codes.json) (321, 310, 300, 527), checked 2 Nov 2026.
- [React source, ReactFiberHooks.js](https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberHooks.js) (`throwInvalidHookError`, `ContextOnlyDispatcher`) and [ReactHooks.js](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js) (development-only message when no dispatcher is set), checked 2 Nov 2026.
- [React source, ensureCorrectIsomorphicReactVersion.js](https://github.com/facebook/react/blob/main/packages/react-dom/src/shared/ensureCorrectIsomorphicReactVersion.js), checked 2 Nov 2026.
- [React docs: Rules of Hooks / invalid hook call warning](https://react.dev/warnings/invalid-hook-call-warning), checked 2 Nov 2026.
- [Vite: `resolve.dedupe`](https://vite.dev/config/shared-options#resolve-dedupe), checked 2 Nov 2026.

## Related errors

- [React: Too many re-renders](/guides/react-too-many-re-renders/): `Too many re-renders. React limits the number of renders to prevent an infinite loop.`
- [React: Maximum update depth exceeded](/guides/react-maximum-update-depth-exceeded/): `Maximum update depth exceeded`
- [ReferenceError: window is not defined](/guides/window-is-not-defined/): `ReferenceError: window is not defined`
- [Next.js hydration mismatch](/guides/nextjs-hydration-mismatch/): `Hydration failed because the server rendered HTML didn't match the client`
- [Next.js Server Components render error](/guides/nextjs-server-components-render-error/): `An error occurred in the Server Components render. The specific message is omitted in production builds`

All guides: [Fix an error](/guides/).

[Start free](/console/register?utm_source=bugtape_seo&utm_campaign=error-guides). Free plan · No credit card · Set up in minutes. Or [try the live demo](/demo/?from=landing&start=1#capture) first.
