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"
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"
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
}
}
- "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;
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";
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;
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";
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";
Finally, what the library will provide when added will be centralized inside the src
folder:
- index.ts
export * from "./components";
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"
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()],
},
];
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"
}
}
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"
}
}
- 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
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
.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
Folders structure
Finally, the initial folder structure will look like this:
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)