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-appmy-component
Start by cloning it down, and doing a cd my-componentyarn installyarn buildnpm linknamepackage.json
Now, go into the cra-appyarn installnpm link my-componentmy-componentcra-appnpm startcra-app
Mission accomplished! Time to go home right!? Let's try just throwing a hook
into MyComponentyarn buildmy-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-componentmy-componentmy-componentnode_modules
All npm link
A fun sanity check is to just delete my-component/node_modules/react
It is my opinion that npm linklnmy-component/dist/index.jsmy-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 linkyarn link
In our example, let's install yalc globally with
npm i yalc -gnpxmy-componentyalc publish
$ yalc publishcomponent-lib@1.0.0 published in store.
Now go back to cra-appyalc 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 startcra-app
Yalc has some pretty comprehensive documentation on how it can be used. Though, here's the cliff notes:
yalc publishyalc add.yalcyalc linknpm linkyarn installOne can add the yalc.lock.yalc
My favorite part of yalc is that the publishcomponent-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 scriptspackage.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 devdist
The commands npm linkyarn linkInvalid hook call
What sorts of cool things are you going to be building using yalc? 😎