linking libraries: react hooks

2021-04-20

 | 

~5 min read

 | 

822 words

Update: A very similar problem can occur when using react-redux. I wrote about it in Linking Libraries: React-Redux Edition.

Setting The Stage

I recently started working on building out a component library at work built in React. To help with local development, I’ve been linking the library into my app so that I can see how things work before publishing the changes.

This was all going well until I added a simple hook to one of my components as a test. This post is about troubleshooting React Hook warnings, particularly when the project is being developed using npm link, yarn link, etc.

My component was a Button:

src/components/Button.tsx
import * as React from "react"

export const Button = (props: { buttonText: string }) => {
  const [val, setValue] = React.useState(0)

  return (
    <button
      onClick={() => {
        console.log("hello, world")
        const newVal = val + 1
        setValue(newVal)
      }}
    >
      {props.buttonText} {val}
    </button>
  )
}

With this hook added, however, I started getting errors when I ran the application. Specifically, an “Invalid hook call” warning:

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.

Debugging The Warning

I started ruling out the issues one at a time.

Following the guidance from the React docs, I ran npm ls react:

% npm ls react
myApp@1.74.0 /Users/stephen.weiss/code/my-app
└── react@17.0.2

This seems to rule out #1 and #3 as options. (I’m 16.8+ and there’s only one version of React.)

Given that my app was bundled with Rollup, I thought possibly the answer would be to ensure rollup didn’t include react and went to mark it as an external, looking at the documentation for peer dependencies as guidance:

tsdx.config.js
module.exports = {
  rollup(config, options) {
    config.external = ["react", "react-dom"]
    return config
  },
}

(Note: The library was bootstrapped with tsdx, which is why I used the tsdx.config.js instead of a rollup config file.)

Unfortunately, this approach threw a different error. Unlike some situations where a new error suggests progress, this was moving the wrong direction. The new error said that a dependency of the library didn’t export a component I was using (not included in the simplified component snippet above).

Having now ruled out numbers #1 and #3 I was forced to consider whether or not the hook was violating one of the rules of hooks. I was pretty confident that wasn’t the case, which left me even more confused. Fortunately, I found another Stack Overflow conversation on the topic that focused me on a paragraph I’d skipped over from the React docs:

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.

Okay, so now we have a new lead! Let’s dig into this one a bit.

Before walking through the steps, we’ll assume that the apps are in fact siblings, example using tree:

% tree -L 1
.
├── my-app
└── my-lib

So, following the steps as prescribed by the React docs, we’ll do the following:

% cd ~/code/my-app/node_modules/react
% yarn link
yarn link v1.22.4
success Registered "react".
info You can now run `yarn link "react"` in the projects where you want to use this package and it will be used instead.
✨  Done in 0.06s.
% cd ~/code/my-lib
% yarn link "react"

Now that the library is linked to app’s React, we can go the other way and create a symlink for the library that the application can consume.

% pwd
~/code/my-lib
% yarn link
yarn link v1.22.4
success Registered "my-lib".
info You can now run `yarn link "my-lib"` in the projects where you want to use this package and it will be used instead.
✨  Done in 0.06s.
% cd ~/code/my-app
% yarn link "my-lib"

Conclusion

Overall, I do not like this answer. As a developer workflow it’s super heavy and prone to error. That said, it makes some sense and I think in time, I can probably make this faster with a shell script that will just “do it all” for me when I want to develop locally! But that’s an optimization for another day. Today, I’m just pleased I got it working and I can move onto the next pressing issue.


Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!