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.
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!
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.
project
│ package.json
|
└───src
│ index.js
└───components
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
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 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.
// 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(),
]
};
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
We need to install SCSS functionality in both Rollup and Storybook. I believe both are reliant on the sass
plugin.
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 - ^
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()]),
}),
//...
]
}
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.
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 works with TypeScript by default and requires no configuration.
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
]
}
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.
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.
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.
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.
- 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
- npmjs - package.json
- PostCSS
- Rollup Config
- Rollup Docs
- Storybook - Docs - All of these docs were very useful, including their reference code.
- Storybook - sass-loader needs to be downgraded - To fix the this.getOptions bug
- Storybook - style-loader and css-loader too!
- Storybook - TypeScript
- TypeScript - What is a tsconfig.json
- TypeScript - Every tsconfig option
... and I'm sure many more I've forgotten!
- @rollup/plugin-typescript
- rollup-plugin-jsx - Not used in the above project
- rollup-plugin-postcss
- rollup-plugin-sass - Use scss, not sass. I'm not sure why. :^)
- rollup-plugin-scss