At RateSetter we have a number of front-end projects that are worked on by different project teams. Each of these projects are designed by our internal designer and have a lot common with them (same inputs, buttons designs etc). Up until now, we had no shared style-sheet or  components, so each of these components had to be created from scratch. I saw a real need for a RateSetter component library. This component library would enable teams to pull down the NPM package, import components, provide some props and have their RateSetter themed component ready to use straight away.

I strongly believed the component would help speed up front-end development by removing the need to write/copy paste components over to new projects and also great for me to learn how to create, managed and get people using a component library used in production throughout multiple engineering teams - so I created RateSetter's component library!

Component Library Overview

There were a number of requirements I had for the component library. Being:

  • Must be React
  • Must house a number of components
  • Must use Sass
  • Must use TypeScript
  • Must be able to bundle the components and be published to an internal NPM registry

Just give me the files

Sometimes it's just easier to clone a repo as opposed to following along with the article creating everything from scratch, I get it. For your convenience I've created a repository which has all the config done for you:

HarveyD/react-component-library
Contribute to HarveyD/react-component-library development by creating an account on GitHub.

Fork it and then give it a star if it helps you out :).

Project Skeleton

I had a look around at a number of React project skeletons I could use to bootstrap my project. But I eventually decided to just create the Component Library from scratch. Here are the libraries I considered:

All of these libraries had excellent documentation and a number of features that made it easy to get different flavours of React projects up and running. But there was always an aspect that wasn't able to be configured easily to meet my requirements (since they abstracted config away). For example, I wanted to use Typescript, but NWB didn't have great support for it. I also wanted to use Sass, but I found it tricky to get Create React Library transforming and bundling the .scss files.

Due to all the above not 100% matching the requirements I had for the component library - I decided to create all the config from scratch!

Project Structure

Since we're creating our  project from scratch, we need to setup the structure. So after setting up npm (npm init) and also setting up git (git init), we will first create the folder structure and the following files:

.gitignore
package.json
rollup.config.js
tsconfig.json
src/
  test-component/
    test-component.tsx
    test-component.scss
    text-component.stories.js
  index.tsx

Now we need to install React:

npm i --save react react-dom
npm i --save-dev @types/react

In test-component.tsx we will have a very simple component:

import React from "react";

import './test-component.scss';

interface IProps {
    theme: 'primary' | 'secondary';
}

const TestComponent: React.FC<IProps> = ({ theme }) => (
  <div className={`test-component ${theme}`}>
    <h1 className="heading">I'm the test component</h1>
    <h2>Made with love by Harvey</h2>
  </div>
);

export default TestComponent;

And in test-component.scss we will have:

.test-component {
    background-color: white;
    border: 1px solid black;
    padding: 16px;
    width: 360px;
    text-align: center;
    
    .heading {
        font-size: 64px;
    }

    &.secondary {
        background-color: black;
        color: white;
    }
}

index.tsx will be used as the entry point for Rollup. Within index.tsx we will need to import and also export TestComponent, this is so when we bundle and export the library - all the components will be too. It will look like:

import TestComponent from "./test-component/test-component";

export { TestComponent };

We will leave test-component.stories.js empty for now.

Adding TypeScript

Run npm i -D typescript and in tsconfig.json add:

{
  "compilerOptions": {
    "outDir": "build",
    "module": "esnext",
    "target": "es5",
    "lib": ["es6", "dom", "es2016", "es2017"],
    "sourceMap": true,
    "allowJs": false,
    "jsx": "react",
    "declaration": true,
    "moduleResolution": "node",
  },
  "include": ["src","images.d.ts"],
  "exclude": ["node_modules", "build"]
}

Alternatively, you can run tsc --init to generate this file and provide your own config.

Adding Rollup

Create rollup.config.js and inside we'll put:

import typescript from 'rollup-plugin-typescript2';
import sass from 'rollup-plugin-sass';
import commonjs from 'rollup-plugin-commonjs';
import external from 'rollup-plugin-peer-deps-external';
import resolve from 'rollup-plugin-node-resolve';

import pkg from './package.json';

export default {
  input: 'src/index.tsx',
  output: [
    {
      file: pkg.main,
      format: 'cjs',
      exports: 'named',
      sourcemap: true,
    },
    {
      file: pkg.module,
      format: 'es',
      exports: 'named',
      sourcemap: true,
    },
  ],
  plugins: [
    external(),
    resolve({
      browser: true,
    }),
    typescript({
      rollupCommonJSResolveHack: true,
      exclude: '**/__tests__/**',
      clean: true,
    }),
    commonjs({
      include: ['node_modules/**'],
      exclude: ['**/*.stories.js'],
      namedExports: {
        'node_modules/react/react.js': [
          'Children',
          'Component',
          'PropTypes',
          'createElement',
        ],
        'node_modules/react-dom/index.js': ['render'],
      },
    }),
    sass({
      insert: true,
    })
  ],
};

This file is the config file that I conjured up after browsing a number of other articles in addition to experimentation to get the config just right for my requirements for the library. The main things to take note of are the addition of the Rollup TypeScript Plugin and the Rollup Sass Plugin.

For this Rollup config to work, we need to install Plugin in addition to some Rollup plugins to get our component library working nicely:

npm i -D rollup rollup-plugin-typescript2 rollup-plugin-sass rollup-plugin-commonjs rollup-plugin-node-resolve rollup-plugin-peer-deps-external

Now in package.json we have to add a couple things, first we have to add config that's used in our rollup.config.js file, under version put:

  "main": "build/index.js",
  "module": "build/index.es.js",
  "jsnext:main": "build/index.es.js",
  "files": [
    "build"
  ],

Yes, we could have hard-coded these values within rollup.config.js, but I think it was better to add them here and import them in the config file. Now we need to create a script that will run our Rollup bundling process:

"scripts": {
  "build" "rollup -c"
}

Now by running rollup -c you should see Rollup perform some magic and create a /build folder that will contain all the assets in our component library, ready for deployment to an NPM registry:

Note: you might want to create a .gitignore file and add node_modules and build to it.

Adding Storybook

Before I set out to create the component library, I heard great things about using Storybook. Storybook is basically a sand-boxed environment for your front-end that helps you to develop your components.

It's not always used for component libraries, but Storybook can be considered as having dual purposes for a component library. It can be used to develop components in addition to being hosted on a website to allow everyone to see how components look/interact without having to clone and setup the repository locally.

The first step to add Storybook was to run the automatic setup command (documented here):

npx -p @storybook/cli sb init --type react

Since we are using TypeScript and Sass for our components, there's some additional config we have to add to get Storybook working nicely.

Navigate to the newly created .storybook directory and create a webpack.config.js file. In this file add:

const path = require('path');

module.exports = async ({ config, mode }) => {
  config.module.rules.push({
    test: /\.scss$/,
    use: ['style-loader', 'css-loader', 'sass-loader'],
    include: path.resolve(__dirname, '../'),
  });

  config.module.rules.push({
    test: /\.(ts|tsx)$/,
    loader: require.resolve('babel-loader'),
    options: {
      presets: [['react-app', { flow: false, typescript: true }]],
    },
  });
  config.resolve.extensions.push('.ts', '.tsx');

  return config;
};

Storybook uses Webpack and if we create this config file, Storybook will use it. We need to install some Webpack loaders to get the above config working:

npm i -D @babel/core babel-loader sass-loader

Since we're are placing our stories within the component's folders and  not all within the storybook directory, we'll need to change the config. Within .storybook/config.js, update the file to be:

import { configure } from '@storybook/react';

// automatically import all files ending in *.stories.js
configure(
  [
    require.context('../src', true, /\.stories\.js$/),
  ],
  module
);

Also delete the stories directory which was generated by the Storybook CLI.

Now, we have to create stories for our TestComponent. Open to src/test-component/test-component.stories.js and place:

import React from "react";
import TestComponent from './test-component';

export default {
  title: "TestComponent"
};

export const Primary = () => <TestComponent theme="primary" />;

export const Secondary = () => <TestComponent theme="secondary" />;

This is a very basic story, showing the two variants of our component.

Storybook should have created a new entry in package.json scripts called, storybook, so run npm run storybook and Storybook will do it's magic and load up your components at localhost:6006.  Storybook should look like:

Adding more components

The way we've structured our project allows for any number of components to be added. Simply copy the test-component folder, rename and then create your new component!

Just remember to add it to index.tsx otherwise it won't be picked up by Rollup.

Additional things

You can (and should) also setup Jest and add tests for your component. But, for the sake of simplicity, I tried to keep everything as minimal as possible in the article to help you get your component library up as fast as possible. I will add a branch into my repository with Jest setup and some example unit tests on components.

Setting up private NPM Registry and Pipeline

The next steps to get this component library out and being used was to setup a private NPM registry and pipeline which automatically publishes to it. I'll be covering this aspect in another blog post which you can read at:  TBD