reactjsnpmvite

How do I troubleshoot my Vite Library (npm package - React Component) throwing "Invalid hook call..."?


I have built an npm package, a reusable ReactJS component using Vite and its library mode. It worked until just over a week ago, when I tried running the local dev server to add some new features via a npm symlink. That's when it suddenly threw Invalid hook call. Hooks can only be called inside of the body of a function component...:

Error displayed in the devtools

As an initial guide for the library I used this post.

Solutions I have tried:
Narrowing down the problem:

The main parts of the project are the lib, src and dist folders.

Main project structure

💡 Since I can implement the component in the src-folder via the relative path (from the lib-folder) without any problems. My best guess is that something with the configuration and dist-folder is causing the error.

package.json

{
  "name": "react-sheet-modal",
  "private": false,
  "version": "1.0.11",
  "description": "An authentic modal sheet for ReactJs - Bringing the ()iOS experience to the web",
  "repository": {
    "url": "git+https://github.com/Colin-Farkas-Personal/React-Sheet-Modal.git"
  },
  "homepage": "https://github.com/Colin-Farkas-Personal/React-Sheet-Modal/blob/main/README.md",
  "keywords": [
    "ReactJs",
    "sheet",
    "modal"
  ],
  "author": {
    "name": "Colin Farkas",
    "email": "colin.farkas@hotmail.se"
  },
  "type": "module",
  "main": "dist/main.js",
  "module": "dist/main.js",
  "types": "dist/main.d.ts",
  "files": [
    "dist"
  ],
  "sideEffects": [
    "**/*.css"
  ],
  "scripts": {
    "dev": "vite",
    "dev:app1": "vite --mode app_1",
    "dev:app2": "vite --mode app_2",
    "build": "tsc -b ./tsconfig.lib.json && vite build",
    "build:watch": "tsc -b ./tsconfig.lib.json && vite build --watch",
    "lint": "eslint .",
    "preview": "vite preview",
    "prepublishOnly": "npm run build"
  },
  "peerDependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  },
  "devDependencies": {
    "@eslint/js": "^9.13.0",
    "@types/node": "^22.9.3",
    "@types/react": "^18.3.11",
    "@types/react-dom": "^18.3.1",
    "@vitejs/plugin-react": "^4.3.3",
    "concurrently": "^9.1.0",
    "eslint": "^9.13.0",
    "eslint-plugin-react-hooks": "^5.0.0",
    "eslint-plugin-react-refresh": "^0.4.13",
    "globals": "^15.11.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "sass": "^1.83.0",
    "typescript": "~5.6.2",
    "typescript-eslint": "^8.10.0",
    "vite": "^5.4.9",
    "vite-plugin-dts": "^4.3.0",
    "vite-plugin-lib-inject-css": "^2.1.1",
    "vite-plugin-restart": "^0.4.2",
    "vite-plugin-svgr": "^4.3.0"
  }
}

vite.config.ts

import { resolve } from 'path';
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import { libInjectCss } from 'vite-plugin-lib-inject-css';
import ViteRestart from 'vite-plugin-restart';
import svgr from 'vite-plugin-svgr';

export default defineConfig({
  plugins: [
    libInjectCss(),
    dts({
      tsconfigPath: resolve(__dirname, 'tsconfig.lib.json'),
    }),
    ViteRestart({
      restart: ['/lib/**'],
    }),
    svgr({
      svgrOptions: {
        ref: true,
        svgo: false,
        titleProp: true,
      },
      include: '**/*.svg',
    }),
  ],
  build: {
    copyPublicDir: false,
    lib: {
      entry: resolve(__dirname, 'lib/main.ts'),
      formats: ['es'],
    },
    rollupOptions: {
      output: {
        assetFileNames: 'assets/[name][extname]',
        entryFileNames: '[name].js',
      },
      external: ['react', 'react-dom'],
    },
  },
  resolve: {
    dedupe: ['react', 'react-dom'],
  },
});

tsconfig.app.json

{
  "compilerOptions": {
    "target": "ES6",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "Bundler",
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedSideEffectImports": true,

    "types": ["vite/client"]
  },
  "include": ["src", "env.ts", "src/vite-env.d.ts"]
}

tsconfig.lib.json

{
  "extends": "./tsconfig.app.json",
  "include": ["lib"],
  "exclude": ["src"]
}

Solution

  • I was able to fix this error by ensuring that no instances of react/jsx-runtime were imported or bundled into the transpiled code.

    The issue was that the main.js file (inside the dist folder) contained react/jsx-runtime, which caused duplicate React export objects since the development environment was also using React.

    I updated the rollupOptions inside vite.config.ts to properly exclude the react/jsx-runtime code:

    export default defineConfig({
      { ... }
      build: {
        copyPublicDir: false,
        lib: {
          entry: resolve(__dirname, './lib/main.ts'),
          formats: ['es'],
        },
        rollupOptions: {
          external: ['react', 'react/jsx-runtime'],
          output: {
            assetFileNames: 'assets/[name][extname]',
            entryFileNames: '[name].js',
          },
        },
      },
    });