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

Viewed 97

Trying to build a react otp component to publish it on npm. Getting this warning on using useState hook while testing the component in a test react app using npm link.

import React, { FC, useState } from "react";
interface Props {
  numInputs: number;
}

const OtpInput: FC<Props> = ({ numInputs }): JSX.Element => {
  const [otp, setOtp] = useState(new Array(numInputs || 4).fill(""));

  return (
    <div>
      {otp.map((_, index) => {
        return (
          <React.Fragment key={index}>
            <input type="number" />
          </React.Fragment>
        );
      })}
    </div>
  );
};

export default OtpInput;

Warning while testing the component:

Warning: 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 OtpInput (http://localhost:3000/main.f021bb100c5d3bb39209.hot-update.js:22:76)
    at header
    at div
    at App

Tried removing react and react-dom from dev dependencies(both 18.2.0) and only having them as peer dependency as suggested in some other similar questions. But it didn't work. However if i remove the useState hook and just render some jsx component, it works fine. Can anyone please explain why am I getting this error?

Edit: As suggested by @AdamThomas Ran npm list react got this

├─┬ @testing-library/react@13.4.0
│ └── react@18.2.0 deduped
├─┬ react-dom@18.2.0
│ └── react@18.2.0 deduped
├─┬ react-otp-input-component@1.0.0 -> ./../react-otp-input-component
│ ├─┬ react-dom@18.2.0
│ │ └── react@18.2.0 deduped
│ └── react@18.2.0
├─┬ react-scripts@5.0.1
│ └── react@18.2.0 deduped
└── react@18.2.0

npm list react-dom returned

├─┬ @testing-library/react@13.4.0
│ └── react-dom@18.2.0 deduped
├── react-dom@18.2.0
└─┬ react-otp-input-component@1.0.0 -> ./../react-otp-input-component
  └── react-dom@18.2.0
1 Answers

In your scenario, easiest fix is to stop using npm link. First npm unlink react-otp-input-component in your app.

Then, instead of linking, define a file: URI in your App package json that points towards the file location of the react-otp-input-component.

Run npm install in your app. When you make changes to react-otp-input-component, you will need to run npm install again in your app.

Unfortunately, symlinks dont play nice with node require strategy...but there are build-tooling specific workarounds.

Alternatives include configuring an alias in your build tooling such that it forces it to use the copy of React in your app package, or using something like yarn portals.

If using TSC, in tsconfig of your app:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "react/*": ["node_modules/react/*"],
      "react-dom/*": ["node_modules/react-dom/*"]
    }
  }
}

This will allow you to keep using symlinks.

The reason symlinking doesnt work by default is node and build tooling on top of it will look upwards from the current file till it hits a copy of react. Because symlinking is linking 2 completely separate logical dependency trees with their own node_modules, when you import react from the lib, this is the copy that is held by the lib. When you import from the app, this is the copy that is held by the app. When doing a file URI install, the lib is copied into your app node_modules so it uses the same one.

Even if its same version, 2 copies get instantiated if they are in a different place on the filesystem, which causes the error. React relies on 1 instantiation since it is a stateful framework (it stores references to the hooks). This is why not using hooks makes it work. When you do use them, the state of those hooks is spread across 2 copies of react, and it gets out of sync.

Related