When developing a shared component library for use across multiple React applications, it’s hard to see your changes across those applications. However, having live reloading in a development environment for imports from a shared component library can speed up development. In this article, I will cover some of my learnings in building this setup with React apps from scratch.
Some time last year, my team decided to break up a rails MVC application into an API service and a React frontend application. This is because the audience we were serving wasn’t always going to be the same, or have the same interaction with our app; if we were trying to serve all audiences at the same time, it would become tricky and chaotic to manage. Plus, our company had adopted a microservice architecture.
Once we split it up, it wasn’t long before the one React frontend application became four separate applications, which meant that we found ourselves building the same component more than once. Since no developer likes writing the same code over and over again, we decided to build a component library that will house common components.
In order to do this, though, we had to solve two major challenges:
- We needed to keep the NPM dependencies of these four applications the same at all times. Simply put, this is because if all four apps are effectively the same thing, they all need to use the same set of dependencies.
- We needed to be able to quickly view or test changes that we make to components in the shared component library. This is so it reduced the amount of work we would have to do when adding or changing a shared component.
We solved the first challenge by adapting Facebook’s create-react-app and used it as a base for all our apps. This allowed us to centralise configuration for running “build”, “test” and even “dev start”. More importantly, this meant we had one
package.json that delivered common dependencies to all four apps reliably. So, at any one point, we could be sure that they all have the same version of any dependency we rely on.
The second challenge, however, is what we struggled with the most, and what I will be solving in this article.
Why testing component libraries in development is hard
One of the difficulties with our setup was that NPM dependencies are pulled from remote sources. This means that when you’re actively working on a component in the shared library, your updates would need to be in Github before they would be pulled into the frontend. In other words, for every change you make, you would have to commit and push to Github on one end, and then run
npm install on the other end to receive the updated version. This slows down development and causes a lot of frustration.
The other difficulty was becoming exhausted with the amount of heavy lifting needed to implement a new component on a frontend. You would work on one thing, and then discover something else that needed to be changed on the shared component. In our case, we would have to rebuild the conditions of the frontend we’re working on in the component library and often have to create mock data, fix what we wanted to fix, and then throw away all of that development code.
I hated how much time it would take just to get setup and be able to start doing the real work, so I started thinking (ie. Googling) about how this could be easier. What I wanted to achieve is when you
import NewSharedComponent from 'shared-library';, any and all changes that you make would be immediately available because you would be getting the latest version of the component from your local files.
Testing assumptions:
The html way: Include bundle in a
<script /> tag
The first idea I had was to try and include the shared library in a frontend using a
<script /> tag. This idea was inspired by how react-rails allows React to be added to rails templates, and in part how jQuery is added to websites - a naive thought, I now know.
This ended up being flawed thinking, because we use Webpack to bundle the Javascript and CSS we write for the front-end applications. As far as I could tell, Webpack only looks for files to import or watch in selected locations, and the
<script /> tags are not included in those locations. In other words, I realised: “That’s not how import works.”
Another issue with this approach was that I would have had to fiddle with advanced webpack configuration. In all honesty, I barely understand what webpack is! It quickly became clear that even if we could make this work, the amount of work that one would have to do before they start writing code was tedious. I did, however, gain a better understanding of what import means.
The NPM way:
npm link
When
<script> tag failed me, I Googled how to include a local NPM package, and - much to no one’s surprise, except mine - it was actually something that people do a lot (both the Googling and trying to link local NPM packages).
The answer I received was very promising: Unlike
<script>,
npm link lets you import things without having to mess with the Webpack config. Basically, you can edit your
package.json and, instead of adding the shared library by Github address, add it by path. Here’s an illustration of that:
Instead of :
"dependencies": { ... "react": "16.8.2" "Shared-library": "git+", ... }
… you would add it like this:
"dependencies": { ... "react": "16.8.2", "shared-library": "file:../shared-library", ... }
This seemed pretty straightforward, so I gave it a try. Since our frontend applications use a shared package for config, and a consolidated
package.json, we had to follow certain steps to use
npm link.
Firstly, in the frontend app, we added:
"dependencies": { "build-tools: file:../build-tools", "shared-library: file:../shared-library", }
Thereafter, in the shared-library directory and the build-tools directory, we ran
npm link.
Then, in the frontend, we could run
npm link ../shared-library and
npm link ../build tools and delete shared-library from dependencies in build-tools.
Lastly, we added
rm yarn.lock && yarn install in the frontend and in build-tools, and
npm install in shared-library.
When all of that works, we could start working on our component. The one drawback with this approach is that we did not get live updates of our changes. To do that, we still needed to run
yarn install on our frontend.
This was a little better than our initial situation, but it still wasn’t great, because it still requires a lot of sitting around, waiting for
yarn install to finish, and then remembering to undo all of those changes. Otherwise, the
package.json might end up in Github, and the relative path we just wrote in
package.json would cause
yarn install to fail when someone else runs it on their computer.
But, even though I didn’t get a great answer, I realised that there was enough out there for a solution to exist. My team continued using the second method for a while, but I knew in the back of my mind that there had to be a simpler way of doing things… and then I noticed something.
The “Aha!” moment: Just use
yarn link
As you might have seen (we didn’t…), we were using both yarn and NPM. I realised that there might be a
yarn link out there which could solve all the things that the above two assumptions couldn’t.
I took a chance and Googled “yarn link”, hoping it was a thing - and it was! Not only was it ‘a thing’, but it was a thing that does exactly what I needed.
With
yarn link, the number of things I had to do was reduced to just two
- Firstly, I needed to run
yarn linkin the shared-library directory.
- And then, I needed to run
yarn link "shared-library"in the frontend I want.
… And voila! I can start developing in the shared-library and frontend, with live reloading!
Going through this, I learned a few key things around Webpack and some of the differences between yarn and NPM: For starters had we started our projects using yarn over NPM, we probably would have arrived at this point much sooner. It also gave us a chance to review our toolset to avoid inconsistencies, like using yarn for some actions and using NPM for others.
Hopefully you can use this to avoid taking the long way around as I did!
Lunga Sizani is a Software engineer at 2U Inc. He has just over six years experience working as a programmer with a variety of technologies, from integration developer to full stack engineer. One of his favourite pastimes, which incidentally happens to also be his day job, is to solve problems he is isn’t officially “qualified” to be solving - because how else does one learn?