Skip to content

Instantly share code, notes, and snippets.

@mitchell-merry
Last active September 5, 2021 18:24
Show Gist options
  • Save mitchell-merry/80b4499c10bb358711f13b7307a5ba2b to your computer and use it in GitHub Desktop.
Save mitchell-merry/80b4499c10bb358711f13b7307a5ba2b to your computer and use it in GitHub Desktop.
Setting up a React Component Library & Challenges in Learning New Technology

Introduction

Recently, I began a new endeavour on a new project that would hopefully expand upon some of my old skills and start to expose new technologies I hadn't yet had the chance to work in. I chose to make a 'UI Kit', or in this case a React Component Library. What I hadn't expected was how difficult it would be to get a project like this off the ground.

My first goal was to create a package that I could include in other projects as a node module that would have many different preset React components included to speed up development time / mimic professional workflows. Because I had never done anything like this, I would need to wrap my head somewhat around many new technologies, including Bundlers, Rollup, Storybook, and eventually TypeScript as I thought it would be a good chance to learn how.

Ultimately, the setup phase involved the following:

  • TypeScript
  • Sass/Scss
  • JSX
  • All of the above plus regular JS and CSS to be (pre)processed and bundled for distribution, using Rollup and various plugins for it
  • Publishing to the NPM registry
  • Installing the library on a separate react app for testing
  • Integrating Storybook to work with all of the above for live testing and documentation.

This Gist aims to capture the most important information I found myself noting down throughout this experiment as a sort of documentation for any future endeavours to sort of, answer questions I may have in the future. In doing so, I will outline the general steps I followed, things I learned, things I needed to take into consideration, any resources I found along the way, and finally my next steps with this.

Installation Notes and General Steps

This is mostly so that I can reflect on this process later (and make sure I don't forget how I did this later!). Forgive me if it seems a bit basic!

Create a new project folder and initialise

npm --init, etc... setup.

Worth noting: the "name" and "version" will become useful later when the package gets published to the NPM registry. The name must be unique to all other packages, and choosing a good starting version / versioning system now will benefit in the long run (e.g. starting at 0.0.1 instead of the default 1.0.0). The same goes for "description", "author", and "license", but those can be changed later.

Initial File Structure

project
│   package.json
|   
└───src
    │   index.js
    └───components

Install React

npm install react react-dom --save-dev

--save-dev saves the dependencies to be only be used for development (i.e. when the package gets installed elsewhere, react will not be re-installed with it (especially if the versions mismatch)). We still want to ensure these dependencies exist wherever it's installed though, so in the package.json we add them to peerDependencies.

Our file structure is updated like so:

  • node_modules, package-lock.json
project
└───node_modules
|   | ...
|   
└───src
|   │   index.js
|   └───components
|
│   package.json
|   package-lock.json

Storybook

npx sb init

Installs Storybook from within the project folder. Storybook can work with many different frameworks, and can detect which you're working inside during installation (Detecting project type.)

This adds the following devDependencies:

"@babel/core"
"@storybook/addon-actions"
"@storybook/addon-essentials"
"@storybook/addon-links"
"@storybook/react" - Because it detected react in our app
"babel-loader"

Adds two scripts:

"storybook": "start-storybook -p 6006" - Starts a development server and run it on port 6006
"build-storybook": "build-storybook" - Allows us to compile the storybook and deploy it

And updates our file structure:

  • .storybook - Holds configuration files for Storybook
  • src/stories - Is created with reference files / example code for Storybook stories.
project
└───.storybook
|   | main.js
|   | preview.js
|
└───node_modules
|   | ...
|
└───src
|   │   index.js
|   └───components
|   └───stories
|       | ...
|
│   package.json
|   package-lock.json

As above, to run our development server, perform npm run storybook. Adding components is detailed later and will cover Storybook.

Rollup

Rollup was by far the most difficult thing to wrap my head around in this. In hindsight, I should have taken more time to understand Rollup independently rather than jumping in the deep end and getting confused with everything else I was trying to do with it (all the plugins).

Warning: explanations in this section are likely to be wrong, naive, or unfinished, based on my own understanding / what I could find. Especially the purpose / functions of plugins and other libraries.

npm install rollup rollup-plugin-babel @rollup/plugin-node-resolve rollup-plugin-peer-deps-external @babel/preset-react rollup-plugin-terser rollup-plugin-postcss --save-dev

  • rollup - The library itself
  • rollup-plugin-babel - Allows us to use the babel with rollup
  • @rollup/plugin-node-resolve - Resolves third party modules if you're using any third party dependencies and add to the source code.
  • rollup-plugin-peer-deps-external - Ensures we exclude and peer dependencies from our bundle (in our case react and react-dom)
  • @babel/preset-react - Handles JSX and other react things for us
  • rollup-plugin-terser - Minimises the bundled JS file.
  • rollup-plugin-postcss - PostCSS allows us to import css files and bundle into the JS. The CSS gets injected into the <head> tag by default. The reason we have a plugin for CSS is because all files other than JS need to have plugins to be imported. We use more later for TypeScript and SCSS.

We need to modify the configuration of rollup, so we do that by creating a rollup.config.js file at the root level of our project.

rollup.config.js

// Import all plugins from here
import babel from 'rollup-plugin-babel';
import resolve from '@rollup/plugin-node-resolve';
import external from 'rollup-plugin-peer-deps-external';
import { terser } from 'rollup-plugin-terser';
import postcss from 'rollup-plugin-postcss';

export default {
  // Entry point for our program
  input: './src/index.js',
  // Array of output files according to settings. In this case we push them to a new folder, 'dist' at the root level
  output: [
    {
      file: 'dist/index.js',
      format: 'cjs',
    },
    {
      file: 'dist/index.es.js',
      format: 'es',
      exports: 'named',
    }
  ],
  // Order is important!
  plugins: [
    // Import CSS and add JS injection code for the CSS
    postcss({
      plugins: [],
      minimize: true, // Minifies the CSS
    }),
    // Babel for all .js and .jsx files (by default), except those in node_modules, using the React preset
    babel({
      exclude: 'node_modules/**',
      presets: ['@babel/preset-react']
    }),
    // The following need to be after all the above pre-processors and plugins (SCSS and TypeScript too, later)
    // Handles peer dependencies and excludes them from our bundle 
    external(),
    // Resolves our third party modules
    resolve(),
    // Minifies the final file for bundling (must be last)
    terser(),
  ]
};

Build Script

Add to the scripts in package.json "build-lib": "rollup -c". This script (executed with npm run build-lib) builds our library according to the config file.

File Structure:

  • rollup.config.js, /dist
project
└───.storybook
|   | main.js
|   | preview.js
|
└───dist
|   | ...
|
└───node_modules
|   | ...
|
└───src
|   │   index.js
|   └───components
|   └───stories
|       | ...
|
│   package.json
|   package-lock.json
|   rollup.config.js

SCSS (Sass)

We need to install SCSS functionality in both Rollup and Storybook. I believe both are reliant on the sass plugin.

Storybook

npm install --save-dev @storybook/preset-scss sass sass-loader@10.1.1 css-loader@5.2.7 style-loader@2.0.0

Adds the following devDependencies:

  • sass
  • @storybook/preset-scss
  • sass-loader@10.1.1 - note that sass-loader needs to be a downgraded version, as per this StackOverflow answer (even though the question is about Vue.js!)
  • css-loader@5.2.7 - need to be downgraded for the same reason as above, per this answer.
  • style-loader@2.0.0 - ^

Rollup

On top of sass, npm install --save-dev rollup-plugin-scss

To use it, the rollup.config.js needs to be modified:

//...imports...
import autoprefixer from 'autoprefixer'
import scss from 'rollup-plugin-scss';

export default {
    //...
    plugins: [
        // Needs to be BEFORE postcss. Transpiles the SCSS files into the CSS files, which then later get transpiled into JS
        scss({
            processor: () => postcss([autoprefixer()]),
        }),
        //...
    ]
}

TypeScript

Install and tsconfig.json

We need two more packages to get TS working. npm install --save-dev typescript tslib

For tsconfig.json, refer to the docs for a more in-depth explanation. I found that the following file worked well:

{
    "compilerOptions": {
        "allowSyntheticDefaultImports": true,
        "module": "esnext",
        "noImplicitAny": true,
        "jsx": "preserve",
        "moduleResolution": "node"
    }
}

module needs to be set to "esnext" for this project, same with moduleResolution to "node" - for resolving modules in node. I set jsx to "preserve" to prevent the typescript transpiler from converting the JSX to regular JS, and leaving that as a job for the babel compiler.

allowSyntheticDefaultImports and noImplicitAny are not required, but work for my use case. View the full tsconfig.json docs here for information on these and every other option.

Note that allowSyntheticDefaultImports may fix some errors you encounter with linters when refactoring the project into TS.

Refactor Project

Now, we refactor our project to use TypeScript. Change index.js -> index.ts and update the entry point in package.json (main). If you have more JavaScript code throughout your project already, you would go through that now and convert them to TypeScript. This includes any Storybook stories you might have.

File Structure:

  • tsconfig.json
  • index.js -> index.ts
project
└───.storybook
|   | main.js
|   | preview.js
|
└───dist
|   | ...
|
└───node_modules
|   | ...
|
└───src
|   │   index.ts
|   └───components
|   └───stories
|       | ...
|
│   package.json
|   package-lock.json
|   rollup.config.js
|   tsconfig.json

Storybook

Storybook works with TypeScript by default and requires no configuration.

Update rollup.config.js

We need to configure Rollup to accept our new input and work with the typescript plugin.

//...imports...
import typescript from '@rollup/plugin-typescript';

// By default, babel and resolve only work/recognise/whatever with files that are either .js or .jsx. To expand this functionality to TypeScript, we pass in the option extensions with .ts and .tsx added as file extensions.
const extensions = ['.js', '.jsx', '.ts', '.tsx'];

export default {
    // Change to .ts
    input: './src/index.ts'
    //...
    plugins: [
        //...scss, postcss
        // I'm fairly sure this needs to be before babel.
        typescript(), 
        babel({
            //...
            extensions
        }),
        resolve({extensions}),
        //...the rest
    ]
}

JSON and Other File Types

Every file type other than .js(x), to my knowledge, needs a plugin to be imported, transpiled, and used. For example, JSON needs @rollup/plugin-json.

Publishing to the NPM registry

It's surprisingly easy to publish to the public NPM registry. This guide is enough to get it up and running. To accommodate for our project, we simply need to modify our package.json:

{
  "main": "dist/index.js",
  "module": "dist/index.es.js",
}

All it takes is adding the module attribute and the associated bundled file. Running npm run build-lib and then npm publish will add you to the registry! You can then install it in your own projects to use the actual components like you would any other package.

Creating a Component

The conventions I use are as follows:

project
└───.storybook
|   | ...
└───dist
|   | ...
└───node_modules
|   | ...
|
└───src
|   │   index.ts
|   |   globals.scss  << Global style sheet
|   └───components
|   |   | ComponentA.tsx << Component here
|   |   | ComponentA.scss
|   └───stories
|       | ComponentA.stories.tsx << Storybook stories defined here
|
│   package.json
|   package-lock.json
|   rollup.config.js
|   tsconfig.json

But honestly I haven't used this enough yet to know what the best structures are! Components are made in exactly the same way you would make them in React, and as for Storybook, follow their docs here.

Reflection

Despite all the effort I've put into this project so far, I currently have almost no components actually written nor much to show for my work. I have been using this as a learning experience and acting like a sponge.

However, I feel that I have set myself up with a perfect opportunity to expand on things like TypeScript and Storybook now as I have perfect my development environment and I'm able to start work without concern. My goal is to have a well-written codebase, well-documented Storybook, and ultimately something I can really be proud of and show off.

I'm excited to properly start work on this project and to see what challenges it brings. Below is a short list of resources I found useful, some of which were linked earlier. Note that it is not complete.

Resources I used

Videos and Guides

  • How to build a React portfolio that gets you a job on profy.dev by Johannes Kettmann - For inspiration of the UI kit project which led me down this rabbit hole in the first place. Also has a lot of dips for aspiring devs on how to stand out, as an aside!
  • Build a React Component Library by Hinam Mehra - One of the first things I found while searching for information on this topic. While the topics were complex and unseen to my eyes when I first read it, it gave me a quick outline of steps and pre-requisites that I needed to figure out before starting. And it made me spend more timing on learning this stuff properly!
  • Build And Publish a React Component Library by PortEXE - A gem of a video I found that outlined the general steps on how to do exactly what I wanted to achieve. It quickly ran over all the steps, but stressed that it's individual parts were not tutorials for the technologies used for them. Allowed me to explore the intracies of the stuff in my own time before continuing, and helped me with the vision I needed to finish the project.
  • React Typescript Tutorial by Ben Awad - I used this to get myself up to speed on enough TypeScript so that I could write the minimal amount of TS needed to get the project running.
  • Create and Deploy NPM Packages by Saiharsha Balasubramaniam

Docs and References

... and I'm sure many more I've forgotten!

Rollup plugins

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment