How to Create a React Library - Easy and Type Safe

In this post you will learn how to create a React library and publish it to NPM with and without Typescript.

Why do we need this?

The first question is why do we need to create a React library at all? Just imagine that you create your React project and you have some shared components. This is totally fine and you don't need to create a React library.

But if you have more than one project then you might want to share some of the components or utility methods between two different projects. If you are working at the company where you have lots of different projects it makes a lot of sense to put all your shareable components in additional project which is a library and reuse it across the whole company.

Another case that you might have is if you want to create some React components and publish them for other people on NPM registry. So other people can install your library, add your components to their project and use them.

The project

Here are 2 files that I already prepared for us. First of all it's .gitignore so we don't commit not needed files.

dist
node_modules

We need it because we will push our changes in remote repository and these are 2 folders that we must ignore. Additionally here I prepared a package.json.

{
  "name": "mla-comps",
  "private": false,
  "version": "1.0.0",
  "description": "",
  "scripts": {
  },
  "repository": {
    "type": "git",
    "url": "git@monsterlessonsacademy.github.com:monsterlessonsacademy/monsterlessonsacademy.git"
  },
  "author": "",
  "license": "ISC",
  "peerDependencies": {
  },
  "devDependencies": {
  }
}

Most important part here is a name mla-comps. Here we must have a unique name because every package on NPM registry must be unique. I always use prefix mla for my projects and then the name of the specific projects.

Adding dependencies

The first thing that we need to do is to add React as a peer dependency in our package.json.

{
  "peerDependencies": {
    "react": "18.2.0"
  }
}

It is important to make it a peer dependency and not just a dependency. As we are building a React library we will use it in React project. Which actually means that React must be installed already in that project this is why it makes a lot of sense to put it to peer dependency.

Button component

Now let's create several components that we want to pack inside our library. Our first component will be a button component.

/* /src/button/button.css */
.mla-button {
  font-size: 16px;
}

Here we created styles for our future button component.

// /src/button/Button.jsx
import './button.css'
export const Button = ({disabled, text, onClick}) => {
  return (
    <button
      type="button"
      disabled={disabled}
      onClick={onClick}
      className="mla-button"
    >
      {text}
    </button>
  )
}

Here we created our button component which gets some props and renders a button.

Organizing our exports

But it is not all. Additionally I want to create an index.js for the Button which will be it's public exports.

// /src/button/index.js
export * from "./Button";

The main point is that inside your component you might have like 20 different child components. For sure you don't want to use all of them outside.

This index.js is an entry point for our button

This is something like a public API which we expose for people. Also we don't want to use later our Button like this.

import Button from 'mla-comps/button/Button'

It is not comfortable and it is difficult to change later. We want to use it like this.

import Button from 'mla-comps'

In order to achieve that we want to have a root file with all our imports.

// /src/index.js
export * from "./button";

This line will reexport everything that we need from button component.

Adding input component

Now we want to do exactly the same with the input.

// src/input/input.css
.mla-input {
  font-size: 18px;
}

Here is our css file.

// src/input/Input.jsx
import { Fragment } from "react";
import "./input.css";

export const Input = ({ disabled, label, onChange }) => {
  return (
    <Fragment>
      <label>{label}</label>
      <input
        type="text"
        disabled={disabled}
        onChange={onChange}
        className="mla-input"
      />
    </Fragment>
  );
};

We created our Input component with some props and rendered a label and an input.

Now we need to export our component.

// src/input/Input.jsx
export * from "./Input";

And add it to the root

// src/index.js
export * from "./button";
export * from "./input";

Building a library

Now we must build our library. There are different tools for that. We can use things like Babel, Rollup but I highly prefer Vite.

Vite is a project generator and a web server

But we can also use Vite in order to build a library.

npm i vite -D

Now we must create a config for Vite.

// vite.config.js
import { resolve } from "path";
import { defineConfig } from "vite";

export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, "src/index.js"),
      name: "mla-comps",
      fileName: "index",
    },
    rollupOptions: {
      external: ["react"],
    },
  },
});

Here in the config we specified our root file and that we want to build React with it.

Under the hood Vite uses Rollup but it is much simpler to use Vite

Now let's add a script to build our library in package.json

"scripts": {
  "build": "vite build"
},

build

As you can see all our modules were traspiled and now in dist folder we get our Javascript and CSS files.

So if you want to publish your project as a Javascript library you are done. At the end of this post I will show how to do it exactly but first of all I want to show you how to write our library with Typescript.

Adding Typescript

Why do we need Typescript? We really want to validate props for all our components and functions. In this case we always know what we must provide in our components this is why it makes a lot of sense to build your libraries with Typescript.

In order to do that we must install 2 dependencies

npm i typescript @types/react -D

Now we must create a config file for Typescript.

{
 "compilerOptions": {
   "target": "es2016",
   "esModuleInterop": true,
   "forceConsistentCasingInFileNames": true,
   "strict": true,
   "skipLibCheck": true,

   "jsx": "react",
   "module": "ESNext",
   "declaration": true,
   "sourceMap": true,
   "outDir": "dist",
   "moduleResolution": "node",
   "allowSyntheticDefaultImports": true,
   "emitDeclarationOnly": true
 }
}

After this we must update our code. First of all we change all index.js files to index.ts and update an entry point for Vite.

entry: resolve(__dirname, "src/index.ts"),

Now let's update our Button component.

import React, { MouseEventHandler } from "react";
import "./button.css";

export interface ButtonProps {
  disabled: boolean;
  text: string;
  onClick: MouseEventHandler<HTMLButtonElement>;
}

export const Button = ({ disabled, text, onClick }: ButtonProps) => {
  return (
    <button
      type="button"
      disabled={disabled}
      onClick={onClick}
      className="mla-button"
    >
      {text}
    </button>
  );
};

We added here an interface ButtonProps which specifies all our properties. Now let's do the same stuff with our input.

import React, { ChangeEventHandler, Fragment } from "react";
import "./input.css";

export interface InputProps {
  disabled: boolean;
  label: string;
  onChange: ChangeEventHandler<HTMLInputElement>;
}

export const Input = ({ disabled, label, onChange }: InputProps) => {
  return (
    <Fragment>
      <label>{label}</label>
      <input
        type="text"
        disabled={disabled}
        onChange={onChange}
        className="mla-input"
      />
    </Fragment>
  );
};

Same story here. We create an interface InputProps and specify all of them.

The last update that we need is to run Typescript after build. Why is that? Vite can transpile Typescript to Javascript but it strips away all data types and leaves just plain Javascript. As we need that we want to call Typescript and create them.

"scripts": {
  "build": "vite build & tsc"
},

Now inside our dist folder we don't only get all transpiled files but also data types for them.

Adding references

The last step before publish is to update our package.json.

{
  "name": "mla-comps",
  "private": false,
  "version": "1.0.0",
  "description": "",
  "main": "dist/index.umd.js",
  "module": "dist/index.mjs",
  "types": "dist/index.d.ts",
  "files": [
   "dist"
  ],
}

All this lines are important so when people use our package bundler know which file to take. Also don't forget an option private: false which allows us to publish to NPM registry for free.

Publish to NPM

In order to publish to the registry you must create there an account. After this you must login in console.

npm login

Inside you must put your credentials and now we can publish our package.

npm publish --access=public

publish

As you can see my package was successfully publish and we can find it on NPM.

Want to conquer your next JavaScript interview? Download my FREE PDF - Pass Your JS Interview with Confidence and start preparing for success today!

📚 Source code of what we've done
Did you like my post? Share it with friends!
Don't miss a thing!
Follow me on Youtube, Twitter or Instagram.
Oleksandr Kocherhin
Oleksandr Kocherhin is a full-stack developer with a passion for learning and sharing knowledge on Monsterlessons Academy and on his YouTube channel. With around 15 years of programming experience and nearly 9 years of teaching, he has a deep understanding of both disciplines. He believes in learning by doing, a philosophy that is reflected in every course he teaches. He loves exploring new web and mobile technologies, and his courses are designed to give students an edge in the fast-moving tech industry.