DEV Community

Eduardo Henrique Gris
Eduardo Henrique Gris

Posted on

React and typescript components lib, part 1: setup rollup and publish

Introduction

Over the past year, I have written articles about various React related topics. Now, the goal is to cover the fundamentals of creating a React library, divided into different parts, applying what was discussed last year along with some new topics: setting up rollup and publishing, using styled-components, adding typeScript, unit testing with Jest and testing-library, integrating eslint and prettier for code standardization, documenting with storybook, setting up pre-commit hooks with Husky, and automating code generation with Hygen.
As I cover topics I've previously written about, I will reference those articles in the ones related to the library. For cases where a new version has introduced significant changes compared to what I previously wrote, I will first publish a dedicated article on the topic before proceeding with its implementation in the library.
Part 1 will cover how to set up the library, create the first example components with typeScript using styled-components, and publish it on npmjs so that others can access it.

Rollup

It is a module bundler for javaScript that allows compiling small pieces of code into larger and more complex entities, such as libraries. It will be used in the creation of the library.

npmjs

Place where the library will be published and made available for other apps to use.

styled-components

A styling library for components in React applications, providing flexibility in defining styles based on received props. I wrote an article last year explaining how it works:
Styled-components in React with Typescript

typescript

Adds typing to a JavaScript app, detecting errors at compile time.

Initial setup

Create a folder with the name of the library (I recommend checking on npmjs to ensure that a library with the same name doesn't already exist, to avoid issues when publishing it), then initialize the project inside it.

yarn init

For now, you can accept all the default settings. Later, the information in the generated package.json will be adjusted.

Add React and styled-components as peer dependencies since they won’t be bundled with the library (the application using the component library will need to have them added). Also, add them as dev dependencies for use during the development of the component library:

yarn add react react-dom styled-components --peer
yarn add react react-dom styled-components --dev

At the time of writing this article, the following versions were generated:

"react": "^19.0.0",
"react-dom": "^19.0.0",
"styled-components": "^6.1.14"
Enter fullscreen mode Exit fullscreen mode

Add typescript to the project

Add TypeScript as a dev dependency:

yarn add typescript @types/react --dev

At the time of writing this article, the following versions were generated:

"@types/react": "^19.0.8",
"typescript": "^5.7.3"
Enter fullscreen mode Exit fullscreen mode

Add the typeScript configuration file to the root of the project.

  • tsconfig.json
{
  "compilerOptions": {
    "target": "ES2016",
    "jsx": "react",
    "module": "ESNext",
    "moduleResolution": "node",
    "declaration": true,
    "emitDeclarationOnly": true,
    "outDir": "dist",
    "declarationDir": "types",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}
Enter fullscreen mode Exit fullscreen mode
  • "target": "ES2016", specify the target ECMAScript version
  • "jsx": "react", transform the jsx code into React
  • "module": "ESNext", generate modern JavaScript modules for the library
  • "moduleResolution": "node", follow the Node.js rules for finding modules
  • "declaration": true, generate a .d.ts file for the library's types
  • "emitDeclarationOnly": true, only export the type declarations without generating js, as Rollup will handle it.
  • "outDir": "dist", directory where the project will be generated
  • "declarationDir": "types", where the .d.ts file will be placed
  • "allowSyntheticDefaultImports": true, assume export default if no export is manually created
  • "esModuleInterop": true, allows better compatibility between CommonJS and ES modules
  • "forceConsistentCasingInFileNames": true, check if the file import is consistent with the case-sensitive file name
  • "strict": true, brings a large number of type checks that ensure excellent code integrity
  • "skipLibCheck": true, ignore the type validation for .d.ts file

Creation of example components

Two example components will be created: one simple for text and another for a tag. First, a src folder will be generated at the root of the project, and inside it, a components folder where all the created components will be placed.

Starting with the text component, a Text folder will be created inside components, with two files:

  • Text.tsx
import React from "react";
import styled from "styled-components";

export interface TextProps {
  children: React.ReactNode;
  color?: string;
  weight?: "normal" | "bold";
  fontWeight?: number;
  fontSize?: string;
  fontFamily?: string;
}

export interface StyledTextProps {
  $color?: string;
  $weight?: "normal" | "bold";
  $fontWeight?: number;
  $fontSize?: string;
  $fontFamily?: string;
}

export const StyledText = styled.span<StyledTextProps>`
  color: ${(props) => 
    props.$color 
      ? props.$color 
      : "#000"};
  font-size: ${(props) => 
    props.$fontSize 
      ? props.$fontSize 
      : "16px"};
  font-weight: ${(props) =>
    props.$fontWeight
      ? props.$fontWeight
      : props.$weight
        ? props.$weight
        : "normal"};
  font-family: ${(props) => 
    props.$fontFamily 
      ? props.$fontFamily 
      : "Arial"};
`;

const Text = ({
  children,
  color,
  weight,
  fontWeight,
  fontSize,
  fontFamily,
}: TextProps) => (
  <StyledText
    $color={color}
    $weight={weight}
    $fontWeight={fontWeight}
    $fontSize={fontSize}
    $fontFamily={fontFamily}
  >
    {children}
  </StyledText>
);

export default Text;
Enter fullscreen mode Exit fullscreen mode

It accepts 5 props: children, which will represent the text itself, color, which will define the text color, fontWeight, which will define the text's font weight, fontSize, which will define the text's font size, and finally fontFamily, which will define the text's font family. If any of these props are not provided, except for children, the StyledText will already have a default value defined for each property.

  • index.ts
export { default } from "./Text";
Enter fullscreen mode Exit fullscreen mode

The component will be made available.

Now, for the tag component, a Tag folder will be created inside components, with two files:

  • Tag.tsx
import React from "react";
import styled from "styled-components";
import Text from "../Text/Text";

export interface TagProps {
  type?: "default" | "success" | "alert" | "error";
  text: string;
  textColor?: string;
  textWeight?: "normal" | "bold";
  textFontWeight?: number;
  textFontSize?: string;
  textFontFamily?: string;
  backgroundColor?: string;
  format?: "default" | "semiRounded" | "rounded";
  borderRadius?: string;
  size?: "small" | "medium" | "large";
  padding?: string;
}

export interface StyledTagProps {
  $type?: "default" | "success" | "alert" | "error";
  $textColor?: string;
  $textWeight?: "normal" | "bold";
  $textFontWeight?: number;
  $textFontSize?: string;
  $textFontFamily?: string;
  $backgroundColor?: string;
  $format?: "default" | "semiRounded" | "rounded";
  $borderRadius?: string;
  $size?: "small" | "medium" | "large";
  $padding?: string;
}

export const StyledTag = styled.div<StyledTagProps>`
  border: none;
  padding: ${(props) =>
    props.$padding
      ? props.$padding
      : props.$size === "large"
        ? "15px 20px"
        : props.$size === "medium"
          ? "10px 12px"
          : "7px"};
  background-color: ${(props) =>
    props.$backgroundColor
      ? props.$backgroundColor
      : props.$type === "error"
        ? "#e97451"
        : props.$type === "alert"
          ? "#f8de7e"
          : props.$type === "success"
            ? "#50c878"
            : "#d3d3d3"};
  pointer-events: none;
  border-radius: ${(props) =>
    props.$borderRadius
      ? props.$borderRadius
      : props.$format === "rounded"
        ? "30px"
        : props.$format === "semiRounded"
          ? "5px"
          : "0"};
  width: fit-content;
`;

const Tag = ({
  text,
  type,
  textColor,
  textWeight,
  textFontWeight,
  textFontSize,
  textFontFamily,
  backgroundColor,
  format,
  borderRadius,
  size,
  padding,
}: TagProps) => (
  <StyledTag
    $type={type}
    $backgroundColor={backgroundColor}
    $format={format}
    $borderRadius={borderRadius}
    $size={size}
    $padding={padding}
  >
    <Text
      color={textColor || "#fff"}
      weight={textWeight}
      fontWeight={textFontWeight}
      fontSize={textFontSize}
      fontFamily={textFontFamily}
    >
      {text}
    </Text>
  </StyledTag>
);

export default Tag;
Enter fullscreen mode Exit fullscreen mode

It accepts 11 props.
3 are pre-defined: size which defines the padding (if large it sets 15px 20px, if medium it sets 10px 12px), type which defines the background-color (if error it sets #e97451, if alert it sets #f8de7e, if success it sets #50c878), and format which defines the border-radius (if rounded it sets 30px, if semiRounded it sets 5px).
And 8 are customizable: text which defines the text of the tag, textColor which defines the text color, textFontWeight which defines the font-weight of the text, textFontSize which defines the font-size of the text, textFontFamily which defines the font-family of the text, backgroundColor which defines the background-color of the tag, borderRadius which defines the border-radius of the tag, and finally padding which defines the padding of the tag.
If any of them are not provided, except for text, the StyledTag already has a default value defined for each property.

  • index.ts
export { default } from "./Tag";
Enter fullscreen mode Exit fullscreen mode

The component will be made available.

After defining the two components, their export will be centralized inside the src/components folder:

  • index.ts
export { default as Tag } from "./Tag";
export { default as Text } from "./Text";
Enter fullscreen mode Exit fullscreen mode

Finally, what the library will provide when added will be centralized inside the src folder:

  • index.ts
export * from "./components";
Enter fullscreen mode Exit fullscreen mode

This way, two components are defined and their exposure is centralized, allowing to proceed with the rollup configuration.

Rollup setup

To use Rollup, the following libraries will first be added:

yarn add rollup rollup-plugin-dts rollup-plugin-peer-deps-external @rollup/plugin-commonjs @rollup/plugin-node-resolve @rollup/plugin-typescript@11.1.6 @rollup/plugin-terser --dev

At the time of writing this article, the following versions were generated:

"@rollup/plugin-commonjs": "^28.0.2",
"@rollup/plugin-node-resolve": "^16.0.0",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "11.1.6",
"rollup": "^4.30.1",
"rollup-plugin-dts": "^6.1.1",
"rollup-plugin-peer-deps-external": "^2.2.4"
Enter fullscreen mode Exit fullscreen mode

Unlike the other libraries being added, a specific version was defined for @rollup/plugin-typescript due to an issue that prevents using it in later versions.

Next, it will be necessary to create the rollup configuration file at the root of the project, which will use the added libraries:

  • rollup.config.js
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import dts from "rollup-plugin-dts";
import terser from "@rollup/plugin-terser";
import peerDepsExternal from "rollup-plugin-peer-deps-external";

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

export default [
  {
    input: "src/index.ts",
    output: [
      {
        file: packageJson.main,
        format: "cjs",
      },
      {
        file: packageJson.module,
        format: "esm",
      },
    ],
    plugins: [
      peerDepsExternal(),
      resolve(),
      commonjs(),
      typescript({ tsconfig: "./tsconfig.json" }),
      terser(),
    ],
    external: ["react", "react-dom", "styled-components"],
  },
  {
    input: "src/index.ts",
    output: [{ file: "dist/index.d.ts", format: "esm" }],
    plugins: [dts.default()],
  },
];
Enter fullscreen mode Exit fullscreen mode

In this file, the libraries added to the app are used.
In input: "src/index.ts", the file where the components are exported is defined. In output, two output files are created: the first in the cjs format and the second in the esm format, which will be specified in package.json with their paths. In peerDepsExternal() and external: ["react", "react-dom", "styled-components"], the external dependencies of the library are defined, meaning these dependencies won’t be bundled when adding the library. In resolve(), Node's algorithm is used to find third-party modules in node_modules. In commonjs(), commonjs modules are converted into es modules, allowing the use of both formats. In typescript({ tsconfig: "./tsconfig.json" }), the integration between rollup and typeScript is defined, and the typeScript configuration file location is provided. In terser(), the size of the bundle is reduced. Finally, the part related to the dts.default() plugin will define an output file with the types used in the library.

package.json

For now, the package.json should look something like the below:

{
  "name": "react-example-lib", // the name of the folder where the project was initialized
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "@rollup/plugin-commonjs": "^28.0.2",
    "@rollup/plugin-node-resolve": "^16.0.0",
    "@rollup/plugin-terser": "^0.4.4",
    "@rollup/plugin-typescript": "^12.1.2",
    "@types/react": "^19.0.8",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "rollup": "^4.30.1",
    "rollup-plugin-dts": "^6.1.1",
    "rollup-plugin-peer-deps-external": "^2.2.4",
    "styled-components": "^6.1.14",
    "typescript": "^5.7.3"
  },
  "peerDependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "styled-components": "^6.1.14"
  }
}
Enter fullscreen mode Exit fullscreen mode

Some changes and additions will be required in it, making it look like this:

{
  "name": "react-example-lib",
  "version": "0.1.0",
  "main": "dist/cjs/index.js",
  "module": "dist/esm/index.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist"
  ],
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/griseduardo/react-example-lib.git"
  },
  "scripts": {
    "build": "rollup -c --bundleConfigAsCjs"
  },
  "devDependencies": {
    "@rollup/plugin-commonjs": "^28.0.2",
    "@rollup/plugin-node-resolve": "^16.0.0",
    "@rollup/plugin-terser": "^0.4.4",
    "@rollup/plugin-typescript": "^12.1.2",
    "@types/react": "^19.0.8",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "rollup": "^4.30.1",
    "rollup-plugin-dts": "^6.1.1",
    "rollup-plugin-peer-deps-external": "^2.2.4",
    "styled-components": "^6.1.14",
    "typescript": "^5.7.3"
  },
  "peerDependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "styled-components": "^6.1.14"
  }
}
Enter fullscreen mode Exit fullscreen mode
  • name: the name of the library is defined.
  • version: the version of the library was changed from 1.0.0 to 0.1.0, as it is not ready for use yet
  • main: the output path for the cjs format
  • module: the output path for the esm format
  • types: the path for the library's types
  • files: the folder where the entire library will be contained
  • repository: where the app is located if you also upload it to github
  • scripts: the build is defined, which will execute rollup

CHANGELOG file

As the library is modified, it becomes useful to show what has changed with each version increment for those who will use it. For this, a file will be created at the root:

  • CHANGELOG.md
## 0.1.0

_Jan. 29, 2025_

- initial config
Enter fullscreen mode Exit fullscreen mode

README file

Another important part is adding a README explaining what the app is about, how to add it, and how to use its components. It will be added to the root:

  • README.md

Image description

.gitignore file

In the case of publishing the library on npmjs and creating a github repository for it, it's important not to upload certain folders to github. A .gitignore file will be added to the root of the project:

  • .gitignore
dist
node_modules
Enter fullscreen mode Exit fullscreen mode

Folders structure

Finally, the initial folder structure will look like this:

Image description

Library publication

The first step to be performed is to check if the rollup execution runs successfully. To do this, yarn build will be executed in the terminal, as defined in package.json. If successful, a dist folder will be automatically created at the root of the project.
After that, it will be necessary to create an account on npmjs, where the library will be published.
Once the account is created on the site, you will need to log in via the terminal inside the project:

npm login

Where you will need to enter the username/email and password (the same as the ones created on the site). After logging in, you can publish the library:

npm publish --access public

Once the publication is successfully executed, the library will be visible on the npmjs site, making it available for other apps to add and use the components defined inside it (Text and Tag). Since react, react-dom, and styled-components were added as peer dependencies, if the app adding the library does not have any of them, it will be informed that these are necessary dependencies for the library to function at the time of addition.

Conclusion

The purpose of this article was to demonstrate how to create and publish a component library in React with TypeScript. To achieve this, it covered how to set up typeScript and configure rollup, defined two components using typeScript with styled-components, presented some files that I believe are useful for tracking changes in the library and showing how to use it, and finally, the steps to publish it on npmjs. The next steps will involve implementing new features in the library, such as tests, pre-commit rules, and others mentioned in the introduction. I am placing the repository on github and publishing it on npmjs to follow along with what I am writing in the articles.

Top comments (0)