A React app throws "Invalid hook call. Hooks can only be called inside of the body of a function component" from a component in a locally linked library, even though that component's code follows the rules. How do you diagnose it?
answer
- the message lists its own three causes
- hooks forward to an installed dispatcher
- two copies means two dispatcher slots
- only breaks when linked
- peer dependency, not dependency
basics
~20 sReact's Invalid hook call error has three documented causes: a rules violation, mismatched React and renderer versions, or more than one copy of React in the bundle. With a linked local library the third is almost always it — the library resolves its own node_modules copy of React.
solid answer
~50 sThe error message itself lists the three causes, and I work them in order of likelihood for the symptom. The code looks correct and only breaks when the library is linked, so I go straight to duplicate copies: `npm link` (or a monorepo without hoisting) leaves the library resolving React from its own `node_modules`, so the app and the library end up with two React module instances. Hooks work through a dispatcher that React installs on itself while rendering — the app's copy installs it, the library's copy reads its own, finds nothing, and throws. I confirm with `npm ls react`, which prints every resolved copy. The fix is to make the library's React a peer dependency (plus a dev dependency for its own builds) so it never bundles or installs its own, and to force single resolution during development — bundler-level deduplication or an alias pointing React at the app's copy. I also check that `react` and `react-dom` are on matching versions.
go deeper
Recognise that this error has more than one cause, and that the message itself lists them. Checking whether a hook is called outside a component is the first thing to rule out.
Explain the mechanism: hooks forward to a dispatcher installed on the React module during render, so two React copies mean the library's hooks read an empty slot. Know that npm ls react reveals it.
Diagnose from the reproduction conditions rather than guessing, then apply a durable fix — peer dependency, React marked external, and bundler-level deduplication for local development — and verify the react/react-dom pairing.
Own the packaging contract for shared libraries: single-instance dependencies like React must be peers and externals by policy, and the local development setup should make duplicate resolution impossible rather than diagnosable.
## Read the error, it is unusually helpful React's message enumerates its own causes: you may have mismatching versions of React and the renderer (such as React DOM), you may be breaking the Rules of Hooks, or you may have more than one copy of React in the same app. Three hypotheses, and the reproduction conditions usually pick one for you. ## Why duplicate copies break hooks at all A hook is not a self-contained function. `useState` is a thin forwarder: it reads the currently installed dispatcher — an internal object of hook implementations that the renderer installs on the React package while a component is rendering, and clears when the render ends — and calls the matching method on it. That is the mechanism behind "hooks only work during render": outside a render there is no dispatcher and the call throws. Now give the process two React modules. `react-dom` is bound to copy A and installs the dispatcher on copy A while rendering. The linked library imported copy B, so its `useState` reads copy B's dispatcher slot, which is empty even mid-render. Every hook in the library throws, while the app's own components work perfectly — which is exactly why the code "looks fine". ## Confirming it `npm ls react` walks the installed tree and prints every place React resolves from; more than one entry, or a `deduped`/nested mix, is the answer. The equivalent exists for other package managers. A second confirmation that needs no tooling: log an identity check across the boundary — if the library and the app import React and the two module objects are not the same object, you have two copies. In a browser, the React DevTools extension warning about multiple React instances is the same finding. ## Why linking causes it so reliably `npm link` (and `file:` installs, and monorepo packages that are not hoisted) creates a symlink to the library's own directory, and Node resolves the library's `import 'react'` starting from that real directory. If the library has React in its own `node_modules` — which it does, because it needs React to build and test — that is the copy it gets. Nothing about this shows up in production installs, where the consumer's install hoists a single copy, which is why the bug appears only in local development and confuses people. ## The fixes, in the order I would apply them **Declare React correctly in the library.** React belongs in `peerDependencies` (so consumers supply it) and in `devDependencies` (so the library can build and test standalone) — never in `dependencies`, which forces a second copy on every consumer. It must also be marked external in the library's bundle output so React is never inlined into the published artifact. **Force single resolution in development.** Point the bundler at one copy: Vite's `resolve.dedupe` accepts package names to resolve from the project root, webpack takes a `resolve.alias` mapping `react` and `react-dom` to the app's paths. Package managers offer install-time equivalents — npm's `overrides`, Yarn's `resolutions`, or `npm dedupe`. **Check the version pairing.** `react` and `react-dom` ship in lockstep and must match; a `react-dom` older than the `react` it renders can produce the same error because the renderer it expects is not the one it finds. `npm ls react react-dom` shows both. ## Do not skip hypothesis two entirely Even with a linked library, spend thirty seconds ruling out a genuine rules violation — a hook called from an event handler, a helper function, or a module-level call at import time. A module-scope `useState()` throws this exact error the moment the module is evaluated, before anything renders, and it is easy to miss in a library entry file. The tell is timing: a rules violation usually throws at a predictable interaction or at import, whereas the duplicate-copy version throws for *every* hook in the library, immediately, uniformly. ## What to say in an interview Structure beats trivia here. Name the three causes from the message, pick the likely one from the reproduction conditions (only after linking → duplicates), state the mechanism (the dispatcher lives on one React module instance), name the confirming command, and give a fix that is permanent (peer dependency + external) rather than a local workaround.
- Why does the same library work fine when installed from the registry but break when linked?A registry install lets the consumer's package manager hoist a single React that the library resolves to. `npm link` symlinks the library's own directory, so Node resolves its `import 'react'` against the library's own `node_modules`, which contains the copy it uses for building and testing. Two module instances, two dispatcher slots, and every hook in the library throws.
- Should React be a dependency, a peerDependency, or both, for a component library?A peer dependency, so the consumer supplies the single copy, plus a dev dependency so the library can build and test on its own. Listing it under `dependencies` guarantees a second installed copy for some consumers. The build must also mark React external so it is never bundled into the published files.
- How would you tell a duplicate-copy failure from an ordinary Rules of Hooks violation without changing any code?Look at the blast radius and the timing. Duplicates break every hook in the affected package uniformly and immediately, and the app's own components keep working. A rules violation is localised to one call site and usually fires at a specific interaction or at module evaluation. `npm ls react` settles it in a second either way.
saying these in an interview costs you the question
- Says the error always means a hook was called conditionally
- Suggests downgrading React instead of deduplicating
- Puts react in the library's dependencies to fix resolution
- Claims react and react-dom versions may drift freely
- Thinks the error only appears in production builds