Ever try using npm link with React? While it works for simple examples, more often it will break when one starts adding hooks into their application. While things like Storybook are great for developing components in isolation, sometimes it makes sense to locally test components in the application that is consuming them. Lets explore why npm (and yarn) link is broken, and how we can use yalc to fix it!
My favorite way of building component libraries is in a monorepo using yarn workspaces. I recently converted craco-babel-loader to use such a style here. What's nice is that since everything is using a package.json as it would in the wild, there's little "magic" that needs to be done outside of just using workspaces. Unfortunately, not all projects, especially enterprise ones can fall neatly into this bucket. So, conventional wisdom says one should probably use npm link or yarn link to develop a package separate from where it would be used.
Note, throughout this, one should be able to substitute the use of npm link for yarn link. You can use either link command in either package manager and will run into just about the same nuances.
Let's try it!
I've created the
following repo as an example. It
contains a standard CRA application in the cra-app
my-component
Start by cloning it down, and doing a cd my-component
yarn install
yarn build
npm link
name
package.json
Now, go into the cra-app
yarn install
npm link my-component
my-component
cra-app
npm start
cra-app
Mission accomplished! Time to go home right!? Let's try just throwing a hook
into MyComponent
yarn build
my-component
import React from "react";export const MyComponent = () => {React.useEffect(() => {console.log("npm link is my best friend!");}, []);return <div>Hello World</div>;};
...and kaboom! Like any epic tragedy, at the end of the first act so falls the non-titular yet somehow the expected hero of the story.
You are probably here for two reasons. Either you're researching tooling for
building component libraries, or you've been burned by this guy after running
npm link
Error: Invalid hook call. Hooks can only be called inside of the body of afunction component. This could happen for one of the following reasons:1. You might have mismatching versions of React and the renderer (such as ReactDOM)2. You might be breaking the Rules of Hooks3. You might have more than one copy of React in the same app Seehttps://reactjs.org/link/invalid-hook-call for tips about how to debug andfix this problem.
That's no fun! Let's take a close look at what the error means:
That's our only remaining option, right? There's a pretty useful command called
npm ls
$ npm ls reactcra-app@0.1.0 /Users/rjerue/dev/yalc-examples/cra-app├─┬ @testing-library/react@11.2.7│ └── react@17.0.2 deduped├─┬ component-lib@1.0.0 extraneous -> ./../component-lib│ └── react@17.0.2 extraneous├─┬ react-dom@17.0.2│ └── react@17.0.2 deduped├─┬ react-scripts@4.0.3│ └── react@17.0.2 deduped└── react@17.0.2
Uh oh! There is indeed two versions of react living inside of our application! There's one inside of our cra app, and there is another in the component library. But why? I have peer dependencies set up correctly and at the very least, they're the same version so it should be deduplicated, right? RIGHT?
Well, if this was treated like a real dependency, yes. However, technically
my-component
my-component
my-component
node_modules
All npm link
A fun sanity check is to just delete my-component/node_modules/react
It is my opinion that npm link
ln
my-component/dist/index.js
my-component/node_modules/react
The library yalc by "wclr" (a.k.a. "whitecolor"
or "alex") was made to solve this problem. In its own words, it acts as a very
simple local repository for your locally developed packages that you want to
share across your local environment and was made specifically to prevent the
problems created by using npm link
yarn link
In our example, let's install yalc globally with
npm i yalc -g
npx
my-component
yalc publish
$ yalc publishcomponent-lib@1.0.0 published in store.
Now go back to cra-app
yalc add component-lib
$ yalc add component-libPackage component-lib@1.0.0 added ==> /Users/rjerue/dev/yalc-examples/cra-app/node_modules/component-lib
From there, running npm start
cra-app
Yalc has some pretty comprehensive documentation on how it can be used. Though, here's the cliff notes:
yalc publish
yalc add
.yalc
yalc link
npm link
yarn install
One can add the yalc.lock
.yalc
My favorite part of yalc is that the publish
component-lib
$ yarn build && yalc publish --pushyarn run v1.22.17$ microbundle --jsx React.createElementBuild "component-lib" to dist:226 B: index.cjs.gz171 B: index.cjs.br173 B: index.modern.js.gz134 B: index.modern.js.br181 B: index.module.js.gz135 B: index.module.js.br311 B: index.umd.js.gz245 B: index.umd.js.br✨ Done in 1.03s.component-lib@1.0.0 published in store.Pushing component-lib@1.0.0 in /Users/rjerue/dev/yalc-examples/cra-appPackage component-lib@1.0.0 added ==> /Users/rjerue/dev/yalc-examples/cra-app/node_modules/component-lib
You should see those changes be hot reloaded into the running cra-app
There are also ways that you can add a watch setup for these commands! As the component library is using microbundle to create a single output folder, we can use a tool like nodemon and npm-run-all to automatically publish changes after a build is done!
Start by installing the following packages:
$ yarn add npm-run-all yalc nodemon --dev
and then add the following under scripts
package.json
{"scripts": {// ... other scripts"dev": "microbundle watch --jsx React.createElement","watch-dist": "nodemon --delay 1 --watch dist --exec \"yalc publish --push\"","start": "run-p dev watch-dist"}}
What this does is it runs the microbundle watcher and nodemon in parallel.
Nodemon will watch the dist
If using another tool to build the library, just replace dev
dist
The commands npm link
yarn link
Invalid hook call
What sorts of cool things are you going to be building using yalc? 😎