DEV Community

Cover image for Component library setup with React, TypeScript and Rollup
Siddharth Venkatesh
Siddharth Venkatesh

Posted on • Updated on

Component library setup with React, TypeScript and Rollup

Introduction

Component libraries are becoming more and more popular by the day, especially at organisations with multiple products and teams. Organisations are dedicating teams just to maintain the component library. The end goal here might be a Design System, with well thought our principles and practices. But, a good design system takes months or even years of research and a dedicated team which a lot of organisation cannot afford. Google's Material design and Atlassian's Design system are some of the excellent ones that come to mind. A good place to start for majority of teams is a component library. A collection of commonly used components which can help attain consistency across applications. We can start out with simple components like button, inputs, modal and add more along the way.

Let's try to build a simple component library from scratch using React, Typescript, and Rollup to bundle it, and learn a thing or two along the way.

Initialise the project

Let's start by creating a directory and initialising an npm project called react-lib

mkdir react-lib
cd react-lib
npm init
Enter fullscreen mode Exit fullscreen mode

You can fill out the questions or pass the -y flag to initialise with default values. We now have a package.json file in our project.

Since we're going to be using react and typescript, we can add those dependencies

npm i -D react typescript @types/react
Enter fullscreen mode Exit fullscreen mode

Since we are going to be shipping this as a library, all our packages will be listed under devDependencies. Also, the app in which this library will be used will come with react, we don't have to bundle react along. So, we'll add react as a peerDependency. Our package.json looks like this now

image

Adding components

My preferred way of organising components is inside src/components folder, where each component would have its own folder. For example, if we have a Button component, there would be a folder called Button in src/components with all the button related files like Button.tsx, Button.css, Button.types.ts, and an index.ts file to export the component

There are also a couple of index files along the way to export stuff. One is the main entrypoint to the project, at src/index.ts, and one which exports all the components at src/components/index.ts. The folder structure with the button component would look like this.
image

Button component

Now, let's add the code for the Button component. I'm going with a very simple component as this is not really our concern right now.

Button.tsx

image

Button.css

image

Button.types.ts

image

Button/index.ts

image

Now that we have our Button component, we can export it from components and from src.

src/component/index.ts
image

src/index.ts
image

TypeScript configuration

We've added our components and now in order to build our library, we need to configure Typescript. We've already installed the typescript dependency, now we need to add the tsconfig.json. We can do this by

npx tsc --init
Enter fullscreen mode Exit fullscreen mode

This creates a tsconfig.json file with most of the available options commented. I use most of the defaults with some minor changes.

{
  "compilerOptions": {
    "target": "es5",
    "module": "esnext",
    "jsx": "react",
    "sourceMap": true,
    "outDir": "dist",
    "strict": true,
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's add a build script in our package.json to test this out.

"scripts": {
    "build": "tsc"
 },
Enter fullscreen mode Exit fullscreen mode

If we run npm run build, we should see a dist folder with all our ts files transpiled into js files. If you notice, there are no css files in dist and they are not bundled by out ts compiler. Let's do that using Rollup

Rollup configuration

We'll be using Rollup as the bundler of choice here. So, lets install it

npm i -D rollup
Enter fullscreen mode Exit fullscreen mode

Plugins

Rollup has a plugin system by which we can specify all the tasks that need to be performed during the bundling process. We'll need the following plugins

  • @rollup/plugin-node-resolve - Resolve third party dependencies in node_modules
  • @rollup/plugin-commonjs - To convert commonjs modules into ES6
  • @rollup/plugin-typescript - To transpile our Typescript code in JS
  • rollup-plugin-peer-deps-external - To prevent bundling peerDependencies
  • rollup-plugin-postcss - To handle our css
  • rollup-plugin-terser - To minify our bundle

Lets install these plugins

npm i -D @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-typescript rollup-plugin-peer-deps-external rollup-plugin-postcss rollup-plugin-terser
Enter fullscreen mode Exit fullscreen mode

rollup.config.js

The next step is to add the rollup.config.js file. This is where all our rollup configs live.

The entrypoint to our library is the src/index.ts file and we'll be bundling our library into both commonjs and es modules formats. If the app using this library supports esmodules, it will use the esm build, otherwise cjs build will be used.

rollup.config.js

import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import { terser } from 'rollup-plugin-terser';
import external from 'rollup-plugin-peer-deps-external';
import postcss from 'rollup-plugin-postcss';

const packageJson = require('./package.json');

export default {
    input: 'src/index.ts',
    output: [
        {
            file: packageJson.main,
            format: 'cjs',
            sourcemap: true,
            name: 'react-lib'
        },
        {
            file: packageJson.module,
            format: 'esm',
            sourcemap: true
        }
    ],
    plugins: [
        external(),
        resolve(),
        commonjs(),
        typescript({ tsconfig: './tsconfig.json' }),
        postcss(),
        terser()
    ]
}

Enter fullscreen mode Exit fullscreen mode

We have defined the input and output values for our cjs and esm builds.

Putting it all together

Notice that we have specified the file option in output from package.json. Let's go ahead and define these two values in package.json

"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",

Enter fullscreen mode Exit fullscreen mode

Now that we've configured Rollup, we can use it in our build script in package.json instead of the tsc command before.

"build": "rollup -c"
Enter fullscreen mode Exit fullscreen mode

If we run npm run build now, we can see that there is a dist folder created with our library output.
image

The cjs folder contains the commonjs bundle and esm folder contains modern esmodules bundle.

We have our own library which can now be published to the npm registry or used with other applications locally as well.

Testing it out

We can test out our library locally by using npm pack or npm link.

Bundling types

If you notice in our dist folder after running npm run build, we can see that we are not bundling our types. The advantage of using TS here is that code editors can pick up the types and provide Intellisense and static type-checking, which is super useful. It also reduces the need to look at documentation often.

We need a add a few options in our tsconfig.json to generate types.

"declaration": true,
"declarationDir": "types",
"emitDeclarationOnly": true
Enter fullscreen mode Exit fullscreen mode

Adding this would add a types folder in our cjs and esm folders in dist.

We can further improve this by providing a single file which would contain all the types used in our library. For this, we are going to be using a Rollup plugin called rollup-plugin-dts which takes all our .d.ts files and spits out a single types file.

npm i -D rollup-plugin-dts
Enter fullscreen mode Exit fullscreen mode

We can add another entrypoint in our rollup.config.js to add our types config.

{
        input: 'dist/esm/types/index.d.ts',
        output: [{ file: 'dist/index.d.ts', format: "esm" }],
        external: [/\.css$/],
        plugins: [dts()],
},
Enter fullscreen mode Exit fullscreen mode

What this does is take the index.d.ts file from our esm bundle, parse through all the types file and generate one types file index.d.ts inside our dist folder.

Now we can add a types entry in our package.json

"types": "dist/index.d.ts"
Enter fullscreen mode Exit fullscreen mode

The entire rollup.config.js looks like this now

import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import { terser } from 'rollup-plugin-terser';
import external from 'rollup-plugin-peer-deps-external';
import postcss from 'rollup-plugin-postcss';
import dts from 'rollup-plugin-dts';

const packageJson = require('./package.json');

export default [
    {
        input: 'src/index.ts',
        output: [
            {
                file: packageJson.main,
                format: 'cjs',
                sourcemap: true,
                name: 'react-ts-lib'
            },
            {
                file: packageJson.module,
                format: 'esm',
                sourcemap: true
            }
        ],
        plugins: [
            external(),
            resolve(),
            commonjs(),
            typescript({ tsconfig: './tsconfig.json' }),
            postcss(),
            terser()
        ],
    },
    {
        input: 'dist/esm/types/index.d.ts',
        output: [{ file: 'dist/index.d.ts', format: "esm" }],
        external: [/\.css$/],
        plugins: [dts()],
    },
]

Enter fullscreen mode Exit fullscreen mode

Now, if we use our library in other projects, code editors can pick up the types and provide Intellisense and type checking.

Conclusion

This is by no means a comprehensive or perfect way to setup a component library. This is just a basic setup to get started and learn about bundling in the process. The next step in this process would be to add tests and tooling like Storybook or Styleguidist.

The source code can be found here react-ts-lib

Thanks for reading!
Cheers!

Top comments (8)

Collapse
 
owuzan profile image
Oğuzhan Yılmaz

How can I use module.scss?

I am writing the following code, but I cannot get the build:

import styles from "./Button.module.scss"
Enter fullscreen mode Exit fullscreen mode

Here is rollup.config.js:

import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import { terser } from "rollup-plugin-terser";
import external from "rollup-plugin-peer-deps-external";
import postcssPlugin from "rollup-plugin-postcss";
import dts from "rollup-plugin-dts";
import scss from "rollup-plugin-scss";
import { defineConfig } from "rollup";

const packageJson = require("./package.json");

export default [
  defineConfig({
    input: "src/index.ts",
    output: [
      {
        file: packageJson.main,
        format: "cjs",
        sourcemap: true,
        name: "react-ts-lib",
      },
      {
        file: packageJson.module,
        format: "esm",
        sourcemap: true,
      },
    ],
    plugins: [
      external(),
      resolve(),
      commonjs(),
      typescript({ tsconfig: "./tsconfig.json" }),
      postcssPlugin(),
      terser(),
      scss(),
    ],
  }),
  defineConfig({
    input: "dist/esm/types/index.d.ts",
    output: [{ file: "dist/index.d.ts", format: "esm" }],
    external: [/\.css$/],
    plugins: [dts()],
  }),
];
Enter fullscreen mode Exit fullscreen mode

declaration.d.ts

declare module "*.scss" {
  const content: Record<string, string>;
  export default content;
}
Enter fullscreen mode Exit fullscreen mode

Here is the error I get in console:

yarn build
yarn run v1.22.21
$ npm run clean && rollup -c

> react-ts-lib@1.0.0 clean
> rimraf dist


src/index.ts → dist/cjs/index.js, dist/esm/index.js...
[!] Error: 'default' is not exported by src/components/Button/Button.module.scss, imported by src/components/Button/Button.tsx
https://rollupjs.org/guide/en/#error-name-is-not-exported-by-module
src/components/Button/Button.tsx (3:7)
1: import { __assign, __rest } from "tslib";
2: import React from "react";
3: import styles from "./Button.module.scss";
          ^
4: export var Button = function (props) {
5:     var _a = props.as, Component = _a === void 0 ? "button" : _a, children = props.children, rest = __rest(props, ["as", "children"]);
Error: 'default' is not exported by src/components/Button/Button.module.scss, imported by src/components/Button/Button.tsx
    at error (/Users/oguzhan/Desktop/react-ui-lib-ts-with-rollup/node_modules/rollup/dist/shared/rollup.js:198:30)
    at Module.error (/Users/oguzhan/Desktop/react-ui-lib-ts-with-rollup/node_modules/rollup/dist/shared/rollup.js:12560:16)
    at Module.traceVariable (/Users/oguzhan/Desktop/react-ui-lib-ts-with-rollup/node_modules/rollup/dist/shared/rollup.js:12919:29)
    at ModuleScope.findVariable (/Users/oguzhan/Desktop/react-ui-lib-ts-with-rollup/node_modules/rollup/dist/shared/rollup.js:11571:39)
    at FunctionScope.findVariable (/Users/oguzhan/Desktop/react-ui-lib-ts-with-rollup/node_modules/rollup/dist/shared/rollup.js:6503:38)
    at ChildScope.findVariable (/Users/oguzhan/Desktop/react-ui-lib-ts-with-rollup/node_modules/rollup/dist/shared/rollup.js:6503:38)
    at MemberExpression.bind (/Users/oguzhan/Desktop/react-ui-lib-ts-with-rollup/node_modules/rollup/dist/shared/rollup.js:8732:49)
    at Property.bind (/Users/oguzhan/Desktop/react-ui-lib-ts-with-rollup/node_modules/rollup/dist/shared/rollup.js:5400:23)
    at ObjectExpression.bind (/Users/oguzhan/Desktop/react-ui-lib-ts-with-rollup/node_modules/rollup/dist/shared/rollup.js:5396:73)
    at CallExpression.bind (/Users/oguzhan/Desktop/react-ui-lib-ts-with-rollup/node_modules/rollup/dist/shared/rollup.js:5396:73)

error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ngvcanh profile image
Ken

Thanks for this guide.

Rollup will compress everything into a single file since it only accepts 1 entry. I tried putting in multiple entries, which got me the .d.ts structure as it was organized in the src folder but the JS didn't.

Is there a way to keep the build JS file structure as it is in the src folder? I mean I want to build individual components.

Collapse
 
hujianboo profile image
BOBO

I have the same problem with you. Now do you have another way to slove it ?

Collapse
 
ngvcanh profile image
Ken • Edited

I create for each component a folder with the folder name being the component name. Inside the components folder I created a .tsx file with the same name as the component. The main code that handles the components I put in this file.

An index.tsx file inside the components directory for a generic export of the component.

In rollup.config.js I write a piece of code that takes all the files with the extension .tsx into an array to fill in the input.

It didn't solve my problem but I can still use individual components.

In my case, all components are direct children of the src folder.

I've been thinking of a solution to write a piece of code for multiple inputs and outputs but I haven't tried it yet.

Thread Thread
 
vrajpaljhala profile image
Vrajpal Jhala • Edited

try preserveModules with combination of preserveModulesRoot config option in rollup

Collapse
 
cmb5557 profile image
CMB

"@rollup/plugin-commonjs - To bundle into commonjs format"

That's not what it does, from the rollup/plugins documentation :
"Convert CommonJS modules to ES6"

It's the complete opposite.

Collapse
 
siddharthvenkatesh profile image
Siddharth Venkatesh

Thank you for pointing it out. Changed it.

Collapse
 
9aaaaaaaaa profile image
9a-aaaaaaaa

Refer to the article, but found some problems can not be used, re-processed the following, if necessary, you can refer to my file. sp