TypeScript

Vite linked React library: fix Invalid hook call

Separate a library bundling mistake from linked-package resolution, keep React external, and prove that the component and renderer use the same React.

Mahdi Ben RhoumaMahdi Ben Rhouma
8 min read

TL;DR#

If a Vite-built component library works alone but throws Invalid hook call after linking it into an application, check React module identity, not just version numbers. Keeping React external prevents it being embedded in the library bundle. A linked library can nevertheless resolve its own development installation while the application renderer uses another copy.

For a Vite consumer, add React and React DOM to resolve.dedupe in the consuming application's configuration. Keep the library's React peer declarations and externalisation too; they solve different parts of the problem. This diagnosis assumes the component obeys the Rules of Hooks and the React/renderer versions are compatible.

The Stack Overflow report already externalised React and React DOM. Recommending that same setting again would not change its build. It also used npm link, which is specifically identified in React's duplicate-copy guidance as a way a bundler can encounter application and library copies.

Why this happens with matching React versions#

An isolated reproduction on 28 October 2026 used Vite 8.3.5, React and React DOM 19.2.4, and Node 26.7.0 on Windows. The library had React peer metadata, an external React import in its built output, and its own physical development copy of React. The consumer had a separate copy of the same version and linked the library through a filesystem junction.

Vite built a client-resolution test bundle. That bundle was executed with ReactDOMServer under Node to exercise a component's hook; this was not a browser test or a Vite SSR-mode build. Before deduplication, the identity comparison was false and rendering produced:

text
SAME_REACT=false
text
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:

The exception then named the hook used by the component:

text
TypeError: Cannot read properties of null (reading 'useContext')

The component called the hook at the top level during rendering. Matching versions and correct hook placement were therefore negative controls: neither prevented the error while the React objects differed.

Fix the consumer's resolution first#

1. Confirm which application is running the library#

Run the dependency-tree check in the consuming application, then in the library directory:

bash
npm ls react react-dom

Look for unexpected nested installations and incompatible peer ranges. Matching version strings do not prove one module instance: the reproduction deliberately copied the same release into two locations. A library's local development dependencies are especially relevant when the consumer follows a link back into that library's real directory.

Also identify the consumer's bundler. Building the library with Vite does not imply that its consumer uses Vite; the original report even contains a webpack-generated identifier. The following Vite option affects a Vite consumer. For another bundler, use its own module-resolution configuration or make the linked library resolve the application's React copy as described by React's official linking guidance.

2. Dedupe in the application that imports the package#

Merge this into the consumer's existing Vite configuration, keeping its plugins and other settings:

js
import { defineConfig } from 'vite';
 
export default defineConfig({
  resolve: {
    dedupe: ['react', 'react-dom'],
  },
});

Vite documents resolve.dedupe for duplicated dependencies caused by hoisting or linked packages. It resolves listed packages from the project root. Restart the consumer after changing its configuration, then test the linked component again. Adding this only to the library's configuration does not transfer the setting to another project's build.

In the reproduction, this was the only configuration change between failing and successful consumer bundles. The library output, peer declarations, installed versions and component code stayed the same. The resulting output was:

text
SAME_REACT=true
<span>shared</span>

The failure exited with an error; the corrected render completed successfully. The library already externalised React before either consumer build, so the test also shows why externalisation alone was insufficient.

3. Keep React external in the library distribution#

Inspect the library output separately. The Vite library-mode guide recommends externalising dependencies that should not be bundled into a library, explicitly including React. For the Vite version tested here, a minimal ES-module library build can be configured as follows:

js
import { defineConfig } from 'vite';
import { fileURLToPath } from 'node:url';
 
export default defineConfig({
  build: {
    lib: {
      entry: fileURLToPath(new URL('./src/index.js', import.meta.url)),
      formats: ['es'],
      fileName: 'index',
    },
    rolldownOptions: {
      external: (id) => /^(react|react-dom)(\/|$)/.test(id),
    },
  },
});

This example expects a JavaScript entry at the stated path; retain the appropriate entry and React/TypeScript plugins for your own library. The predicate also matches package subpaths, avoiding a narrow exact-name list if the output imports React's JSX runtime. Verify the resulting files instead of assuming the configuration reached the entry that your package exports.

The source question uses the older build.rollupOptions spelling. Current Vite build options document build.rolldownOptions; use the option appropriate to the installed release. Do not copy an old configuration block blindly into an unrelated current setup.

4. Declare the React versions the host must supply#

Use peerDependencies to describe the React compatibility contract of the published library. npm documents peer dependencies as compatibility with a host package. Set the range to releases you actually support and test; copying a broad range does not establish compatibility.

Development dependencies may still provide React for the library's own tests and examples. That is why moving a declaration into peerDependencies is useful package design but not proof that a linked development tree resolves one copy. The failing fixture already had peers. Check the packaged distribution and the consuming resolver as well as the manifest.

Verify the module, not only a successful library build#

During diagnosis, temporarily expose the library's React value from a diagnostic entry:

js
import React from 'react';
export const libraryReact = React;

In the consumer, compare that value with its own React import and render a small library component that uses a hook. The reproduction used this comparison plus a context-reading component; both were needed. A successful library build alone never executed the consumer's renderer and therefore could not reveal the runtime mismatch. Remove the diagnostic export when finished.

If exporting diagnostics alongside a component triggers the separate Fast Refresh export warning, keep the diagnostic module separate. That lint warning concerns export boundaries; disabling it does not merge React instances.

There are limits to this remedy. Vite documents a deduplication caveat for ESM SSR build outputs. The client-resolution reproduction above does not establish a fix for every SSR externalisation setup. Likewise, multiple independent React applications can exist on a page; the problem is a component and its renderer using different React exports within the same rendering relationship.

If the error changes to a linked module not providing an export, inspect the library's ES-module output and package entry rather than continuing to tune hooks. If Vite instead cannot resolve a dependency's entry file, follow the package-entry diagnosis. Those failures occur during module loading; this failure occurs when the renderer calls a component whose hook comes from another React instance.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.