Adding a React Components Package to a Monorepo

Matti Bar-Zeev - Dec 24 '21 - - Dev Community

In this post I will be adding a React components package to an existing monorepo (along with Storybook) and automatically publishing it to NPM using GitHub actions, with a somewhat disturbing surprise at the end ;)
The monorepo is my “Pedalboard” monorepo which is described in more detail in this post.

The component I chose to add is my worn down, truly nothing to write back home about, Pagination component, but hey - still a React component :) let’s get started -

I will do this manually though I recommend that if your monorepo is supposed to be used by many developers and hold many components, you should have a sort of a package generator for it, so that the packages created are aligned and the dev experience is good.

I will start with adding a “components” directory under the “packages” directory, and in it an “src” directory.
Then I will cd that directory and initialize yarn like so -

cd packages/components && yarn init
Enter fullscreen mode Exit fullscreen mode

After the initial package.json was created, I will change its name to include the @pedalboard namespace, add Jest and Eslint as dev dependencies and mark its publish config as “public” (so it will not inherit the root project’s “private” configuration). Here is how my package.json looks like now:

{
   "name": "@pedalboard/components",
   "version": "0.0.0",
   "description": "A set of well-crafted components",
   "main": "index.js",
   "author": "Matti Bar-Zeev",
   "license": "MIT",
   "publishConfig": {
       "access": "public"
   },
   "scripts": {
       "test": "echo \"Still no test specified\" && exit 0",
       "lint": "eslint ./src"
   },
   "devDependencies": {
       "eslint": "^8.4.1",
       "jest": "^27.4.3"
   }
}
Enter fullscreen mode Exit fullscreen mode

(We are still not running any tests so the yarn test script is does nothing)

I will also create an “index.js” file in the root directory of the package which will act as the export barrel for all the future components this package will have.

As mentioned, the component is the Pagination component, which is a simple component for pagination (You can read more details about it in a previous post, where I migrated it from render-props to Hooks).
It is a React component so we start with fetching React and ReactDOM packages as dependencies, but these are peer dependencies since we assume that whomever will use this components package will do it from a project which already has them.

Yarn add react react-dom -P
Enter fullscreen mode Exit fullscreen mode

Now I will add my component’s source code to the Pagination directory (you can check the GitHub repo for the source code), but while we have the entire logic for the component we still don’t have any means to render it on screen. For this purpose I would like to have Storybook for my components - let’s get that working for us:

Following Storybook’s docs I will init it on the components package -

npx sb init
Enter fullscreen mode Exit fullscreen mode

Right… all of the Storybook dependencies were hoisted to the root project’s node_modules (we’re using Yarn Workspaces, remember?), and I got a new “stories” directory under the “src” directory of the components package.
In addition to that, the init process added 2 new npm scripts to my package.json file:

"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
Enter fullscreen mode Exit fullscreen mode

It appears that new versions of Storybook have an issue with react and react-dom as peer dependencies. The current suggestions I found was to either put these dependencies in the dependencies section of the package.json or downgrade the version of Storybook. Let's admit it, both suck. I tried something and it seems to be working - in the components package react and react-dom are still peer dependencies, but on the monorepo's root project I have them as dependencies. Yeah, I think that this is a less horrible solution...

I guess it is time to see if anything works, right?

Yarn storybook
Enter fullscreen mode Exit fullscreen mode

Yep! We’re up and running with some out-of-the-box stories.
It is time to write my own story for the Pagination component just to make sure it all works as expected:

import React from 'react';
import Pagination from '../Pagination';

// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
export default {
 title: 'Example/Pagination',
 component: Pagination,
 // More on argTypes: https://storybook.js.org/docs/react/api/argtypes
 argTypes: {
   onPageChange:{ action: 'Page changed' },
 },
};

// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template = (args) => <div><Pagination {...args} /></div>;

export const Simple = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args
Simple.args = {
   pagesCount:10,
   cursor:3,
   pagesBuffer:5,
};
Enter fullscreen mode Exit fullscreen mode

And here is my component:

Image description

Nothing too fancy with some functionality issues, but this is a good start :) I have a component on my monorepo which gets included in my component catalog (a.k.a Storybook).

Before we go and commit these changes, let’s set our ESlint configuration for this package to suit the React nature of it. I will add the eslint-plugin-react plugin to the package’s dev dependencies

yarn add eslint-plugin-react -D
Enter fullscreen mode Exit fullscreen mode

And my .eslintrc.json looks like this:

{
   "env": {
       "browser": true,
       "commonjs": true
   },
   "extends": ["eslint:recommended", "plugin:react/recommended"],
   "parserOptions": {
       "ecmaVersion": 2020,
       "sourceType": "module",
       "ecmaFeatures": {
           "jsx": true
       }
   },
   "rules": {}
}
Enter fullscreen mode Exit fullscreen mode

(sourceType as modules to support ESM import/export and ecmaFeatures including “jsx” for React)

So if I push this components package to my GitHub repo, will it get published as part of my Monorepo’s GitHub action? Let’s check -

I add all the changes and give my commit message:

feat: Add the first component to the components package
Enter fullscreen mode Exit fullscreen mode

GutHub action starts the pipeline, and sure enough it got published to NPM:

Image description

Nice :)

Wait… I’m noticing that although I did not change the other package I have on the monorepo (eslint-plugin-craftsmanslint) it still got version bumped and published to NPM. This is a disturbing surprise. Why is this happening?
From this thread I’m learning that it might be to do with the fact that when the GitHub action checks out the code it does not fetch the entire tags history with it, and therefore Lerna has a hard time determining what really changed. So I’m adding to the checkout command on the npm-publish.yml that instruction.

- uses: actions/checkout@v2
      with:
        fetch-depth: 0
Enter fullscreen mode Exit fullscreen mode

Let’s try now to publish some fixes to the components package and see if only the components package gets updated and published. I added an empty test just for the sake of it, and pushed it…

Yes! Only the components package was published.

So there we have it:
A new components package under the Pedalboard monorepo with a single (and quite ugly I admit) component in it. We also have a cool Storybook catalog which can display this component and any future one to come. We have Eslint working and even an empty test ;) we made sure that only the packages that were changed get published to npm by fetching all the tags history when checking out the code on our GitHub action and… I think that this is it.

Of course, all recent code resides on the Pedalboard monorepo so you can check the recent code changes there, and as always if you have any ideas on how to make this better or any other technique, be sure to share with the rest of us!

Hey! If you liked what you've just read check out @mattibarzeev on Twitter 🍻

Photo by Robin Glauser on Unsplash

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .