React + Antd + Rollup Component Library "Error: Invalid hook call. Hooks can only be called inside of the body of a function component"
Asked Answered
V

5

8

I'm currently building a UI library to simplify maintenance across multiple applications. These currently use Ant Design.

All seemed to go fine... I added my peer dependencies in both package.json and rollup.config.js (via externals) and I was able to get Rollup to produce an es and cjs binary which successfully exports just my code.

However, when I import either of these into my host application (Electron and/or React, already using antd without issue) I am receiving the following error:

Uncaught 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:
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://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
    at resolveDispatcher (react.development.js:1476)
    at Object.useContext (react.development.js:1484)
    at Button (button.js:129)
    at renderWithHooks (react-dom.development.js:14985)
    at updateForwardRef (react-dom.development.js:17044)
    at beginWork (react-dom.development.js:19098)
    at HTMLUnknownElement.callCallback (react-dom.development.js:3945)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:3994)
    at invokeGuardedCallback (react-dom.development.js:4056)
    at beginWork$1 (react-dom.development.js:23964)
resolveDispatcher @ react.development.js:1476
useContext @ react.development.js:1484
Button @ button.js:129
renderWithHooks @ react-dom.development.js:14985
updateForwardRef @ react-dom.development.js:17044
beginWork @ react-dom.development.js:19098
callCallback @ react-dom.development.js:3945
invokeGuardedCallbackDev @ react-dom.development.js:3994
invokeGuardedCallback @ react-dom.development.js:4056
beginWork$1 @ react-dom.development.js:23964
performUnitOfWork @ react-dom.development.js:22779
workLoopSync @ react-dom.development.js:22707
renderRootSync @ react-dom.development.js:22670
performSyncWorkOnRoot @ react-dom.development.js:22293
scheduleUpdateOnFiber @ react-dom.development.js:21881
updateContainer @ react-dom.development.js:25482
(anonymous) @ react-dom.development.js:26021
unbatchedUpdates @ react-dom.development.js:22431
legacyRenderSubtreeIntoContainer @ react-dom.development.js:26020
render @ react-dom.development.js:26103
(anonymous) @ renderer.tsx:129
./src-template/renderer.tsx @ renderer.tsx:150
__webpack_require__ @ bootstrap:789
fn @ bootstrap:100
0 @ renderer.tsx:150
__webpack_require__ @ bootstrap:789
(anonymous) @ bootstrap:856
(anonymous) @ bootstrap:856
react-dom.development.js:20085 The above error occurred in the <Button> component:

    at Button (http://localhost:3000/main_window/index.js:48908:30)
    at ../../ui-library/dist/index.cjs.js.exports.ComponentA (http://localhost:3000/main_window/index.js:101188:13)
    at div
    at App (http://localhost:3000/main_window/index.js:204727:30)

I cannot understand how to proceed... I've tried to tweak my rollup config (below) and strip out all my code down to just a single tester component (antd Button) yet I still experience the error.

When I console.log() the import object I can see that both the es and cjs binaries exposes the tester component but the error is present.

What am I missing here?

Peer dependencies

  • React
  • React DOM
  • antd
  • @ant-design/icons

Rollup.config.js

import { DEFAULT_EXTENSIONS } from '@babel/core'
import babel from '@rollup/plugin-babel'
import typescript from 'rollup-plugin-typescript2'
import commonjs from '@rollup/plugin-commonjs'
import external from 'rollup-plugin-peer-deps-external'
import postcss from 'rollup-plugin-postcss'
import resolve from '@rollup/plugin-node-resolve'
import url from '@rollup/plugin-url'
import svgr from '@svgr/rollup'
import { terser } from 'rollup-plugin-terser'

import pkg from './package.json'


const isDevelopment = process.env.NODE_ENV === 'development' ? true : false;

console.log('EXPECTED EXTERNALS', [
      ...Object.keys(pkg.dependencies || {}),
      ...Object.keys(pkg.peerDependencies || {})
])
export default {
  input: 'src/index.jsx',
  output: [
    {
      file: `dist/index.es.js`,
      format: 'esm',
      exports: 'named',
      sourcemap: isDevelopment,
    },
    {
      file: `dist/index.cjs.js`,
      format: 'cjs',
      exports: 'named',
      sourcemap: isDevelopment,
    }
  ],
  context: 'this',
  external: [
        ...Object.keys(pkg.dependencies || {}),
        ...Object.keys(pkg.peerDependencies || {})
  ],
  plugins: [
    external(),
    typescript({
      rollupCommonJSResolveHack: true,
      clean: true,
      tsconfig: './tsconfig.json',
    }),
    babel({
      presets: [
        'react-app',
      ],
      extensions: [
        ...DEFAULT_EXTENSIONS,
        '.ts',
        '.tsx',
      ],
      plugins: [
        ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }],
        "@babel/plugin-proposal-object-rest-spread",
        "@babel/plugin-proposal-optional-chaining",
        "@babel/plugin-syntax-dynamic-import",
        "@babel/plugin-proposal-class-properties",
        "transform-react-remove-prop-types"
      ],
      babelHelpers: 'runtime',
    }),
    postcss({
        extensions: ['.css', '.scss', '.less'],
        use: ['sass', ['less', {
          lessOptions: {
             javascriptEnabled: true
          }
        }]],
    }),
    svgr(),
    url(),
    resolve(),
    commonjs(),
    terser({ mangle: true }),
  ],
}

Package.json (component library)

{
  "name": "ui-library",
  "version": "0.0.1",
  "description": "UI library components",
  "main": "index.js",
  "scripts": {
    "build": "rm -rf dist && mkdir dist && NODE_ENV=production BABEL_ENV=production rollup -c"
  },
  "peerDependencies": {
    "@ant-design/icons": "^4.3.0",
    "antd": "^4.9.2",
    "react": "^17.0.1",
    "react-dom": "^17.0.1"
  },
  "devDependencies": {
    "@babel/core": "^7.12.10",
    "@babel/plugin-proposal-class-properties": "^7.12.1",
    "@babel/plugin-proposal-object-rest-spread": "^7.12.1",
    "@babel/plugin-proposal-optional-chaining": "^7.12.7",
    "@babel/plugin-syntax-dynamic-import": "^7.8.3",
    "@babel/plugin-transform-react-jsx": "^7.12.10",
    "@babel/preset-env": "^7.12.10",
    "@babel/preset-react": "^7.12.10",
    "@babel/preset-typescript": "^7.12.7",
    "@rollup/plugin-babel": "^5.2.2",
    "@rollup/plugin-commonjs": "^17.0.0",
    "@rollup/plugin-node-resolve": "^11.0.0",
    "@rollup/plugin-typescript": "^8.0.0",
    "@rollup/plugin-url": "^6.0.0",
    "@svgr/rollup": "^5.5.0",
    "@types/node": "^14.14.11",
    "@types/react": "^17.0.0",
    "@types/react-dom": "^17.0.0",
    "@typescript-eslint/eslint-plugin": "^4.9.1",
    "@typescript-eslint/parser": "^4.9.1",
    "babel-loader": "^8.2.2",
    "babel-plugin-import": "^1.13.3",
    "babel-plugin-transform-react-remove-prop-types": "^0.4.24",
    "babel-preset-react-app": "^10.0.0",
    "css-loader": "^4.2.1",
    "eslint": "^7.15.0",
    "eslint-config-airbnb-typescript": "^12.0.0",
    "eslint-plugin-react": "^7.21.5",
    "less-loader": "^7.1.0",
    "mini-css-extract-plugin": "^1.3.2",
    "react-is": "^17.0.1",
    "rollup": "^2.34.2",
    "rollup-plugin-peer-deps-external": "^2.2.4",
    "rollup-plugin-postcss": "^4.0.0",
    "rollup-plugin-terser": "^7.0.2",
    "rollup-plugin-typescript2": "^0.29.0",
    "sass-loader": "^10.1.0",
    "style-loader": "^2.0.0",
    "typescript": "^4.1.2",
    "url-loader": "^4.1.1"
  }
}

Component library tester component

import React from 'react';

import { Button, Radio } from 'antd';
import { DownloadOutlined } from '@ant-design/icons';
import { SizeType } from 'antd/lib/config-provider/SizeContext';


export interface ButtonProps {
  /**
   * What background color to use
   */
  backgroundColor?: string;
  /**
   * Button contents
   */
  label: string;

  /**
   * Size (small | large)
   */
  size: SizeType;
  /**
   * Optional click handler
   */
  onClick?: () => void;
}


// export const ComponentA = (props: ButtonProps) => (<button type="button" onClick={props.onClick} style={{ backgroundColor: props.backgroundColor}}>{ props.label }</button>);

export const ComponentA = (props: ButtonProps) => (
  <Button
    type="primary"
    shape="round"
    icon={<DownloadOutlined />}
    size={props.size || 'middle'}
    onClick={props.onClick || null}
  >
    {props.label || ''}
  </Button>
)

UPDATE: Added rollup-plugin-visualizer output

enter image description here

Verile answered 11/12, 2020 at 21:45 Comment(4)
Also, if you want to check the bundle content try to install rollup-plugin-visualizer, this way you can be sure that react it is not part of the bundle.Infarct
Hi @lissettdm, thank you for your suggestions. Sadly, explicitly setting the externals array had no effect and I can see that React or ReactDOM is not in the visualisation from rollup-plugin-visualizer (see original post for update)Verile
Are you using npm link?Infarct
@Infarct just about to try againVerile
C
29

If this issue happens while you're linking the local version of your library in your main project to speed up the development. It might be related to "duplicate version of React".

https://reactjs.org/warnings/invalid-hook-call-warning.html

This problem can also come up when you use npm link or an equivalent. In that case, your bundler might “see” two Reacts — one in application folder and one in your library folder. Assuming myapp and mylib are sibling folders, one possible fix is to run npm link ../myapp/node_modules/react from mylib. This should make the library use the application’s React copy.

In short:

  • run npm link in /your-app/node_modules/react. This should make the React's global link.
  • run npm link react in /your-ui-library. This should make the library use the application’s React copy.
Centipoise answered 11/12, 2020 at 22:16 Comment(7)
Hi... Just to confirm npm link in my-app/node_modules/react and then npm link react in the ui-library repository on my computer? Then build and run my-app again?Verile
Okay, so as above I did the npm link and it has fixed the error for now. I am going to try publishing to GitLab and see if the lib still works or if I can reproduce this problem somehow. I presume that I will need to keep repeating this step?Verile
This has happened to me several times and I have never tried this option, good to know.Infarct
It is a narrow fix. Going to be frustrating remembering to do this each time I npm install though. Going to see what happens when I publish it to my registry and install the package as a dependency, wonder if this step is exclusively for importing locallyVerile
Yes, I mentioned in my answer that the error can be caused by the local development. I don't think that you will face this issue when you publish the library. This step, in theory, should be avoided anyway because your ui-library components should be developed in isolation and not dependent on your main project. You can use a tool like Storybook to help you with this and it's also a great way to document the use cases of your ui-lib components.Centipoise
@NemanjaLazarevic yes I intended to have this library in isolation, didn’t want to keep pushing to git on every change so installed it as a file dependency into my applications. Storybook was being used, but there’s another issue there with react 17 so temporarily removed!Verile
After 3 days of fighting with the builds, I got it working with this fix. Thank you!!!Microtome
A
4

Try moving react and react-dom to peerDependencies in package.json

guide: https://dev.to/alexeagleson/how-to-create-and-publish-a-react-component-library-2oe#optimizing

"peerDependencies": {
  "react": "^18.1.0",
  "react-dom": "^18.1.0"
}
Altissimo answered 14/6, 2022 at 11:36 Comment(2)
This helps to reduce the build size but does not resolve the problem.Microtome
@Microtome In my case without peerDependencies, my custom package would create it's own node_modules and inside it would have react package and this was causing confusion as there were two react packages in project, can you check this? this might help you to solve your issue.Altissimo
E
2

I too encountered the same error and since I am using @rollup/plugin-node-resolve I just used the dedupe option.

// some imports
import { nodeResolve } from '@rollup/plugin-node-resolve';

export default {
  // some options
  plugins: [
    // some plugins
    nodeResolve({
      // some node resolve options
      dedupe: ['react']
    })
  ]
}

ref: https://www.npmjs.com/package/@rollup/plugin-node-resolve#dedupe

Esquiline answered 9/3 at 7:18 Comment(0)
M
1

I have the same issue, working with a rush monorepo, configuring ui-package and building the apps using nextjs.

The solutions was add the follow code on package.json

"resolutions": {
  "react": "17.0.1", // verificate your react version
  "react-dom": "17.0.1"
}

See more here: https://github.com/vercel/next.js/issues/9022#issuecomment-728688452

Metre answered 3/8, 2021 at 4:9 Comment(1)
In my case using a rush monorepo, I added this to package.json in the root of the monorepoMetre
R
0

I encountered the same problem with just Antd. I was working on a test application recently created with:

npx create-react-app my_project --template typescript

The application was un-ejected. I then added import {Button} from 'antd' in some file and started using the Button. Strangely, npm run start didn't complain that it couldn't find the antd library but when I was trying to run the application I was getting the "invalid hook call" message shown above.

Running:

npm i --S antd

… fixed it for me. I still don't have an explanation on why npm run start didn't complain when I initially failed to add antd in my package dependencies.

Rajkot answered 24/10, 2023 at 11:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.