Home

Fix this error · React and Next.js

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

Invalid hook call (#321) means a hook ran outside render or against a second copy of React. How to find which, fix it and dedupe React.

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.

FactDetail
ErrorInvalid 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 buildsMinified React error #321, or Cannot read properties of null (reading 'useState') in Chrome when there are two copies of React
Where you see itBrowser console, the Next.js error overlay, test output
Usual causeA hook called in an event handler, a class component or a plain function, or two copies of React in the bundle
Usual fixCall hooks at the top of a component or custom hook, and dedupe React
BugTape catches itYes, 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:

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.

// 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.

// 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.

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

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):
{
  "overrides": {
    "react": "$react",
    "react-dom": "$react-dom"
  }
}

  • With linked packages in Vite, make the bundler use the app's copy:
// 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

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:
'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):

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

All guides: Fix an error.

Start free. Free plan · No credit card · Set up in minutes. Or try the live demo first.