Featured

How to Create a Custom Component Library in React

To create your own React component library, you need to perform several steps.

  1. You need to initialize your library.
  2. Then you configure the build system.
  3. You implement the components of the library.
  4. In the final step, you determine how to make the library available.

When using React, sooner or later you’ll integrate libraries to extend its functionality. You use React Router to navigate your application, Redux for centralized state management, or Material UI to access a collection of prebuilt components. Libraries for React all have the same goal: they create reusable structures to solve standard problems at a higher level than just that of a single application.

 

If you work in an organization where multiple React applications are being developed, you’ll quickly find that all development teams use a basic set of very similar components. However, you can avoid this multiple work by creating and developing these components in a central location. You can then integrate this central library into your applications via your package manager, such as NPM, for example, and use it there.

 

The library we create provides a component as well as a hook function. It’s implemented based on TypeScript, the components have their own styles and tests, and you use Storybook to display the components before they’re integrated.

 

Initializing the Library

The basis for the library is a new directory named library. In this directory, you first create a .gitignore file with the content from this listing to prevent node_modules from being integrated into your repository:

 

node_modules

 

In the next step, you use the npm init -y command to create a package.json file for your library. The command creates a default version of the description file for your library, which you can adjust somewhat, as you can see:

 

{

   "name": "library",

   "version": "1.0.0",

   "description": "Utility library",

   "scripts": {},

   "license": "ISC"

}

 

At this point, the only important thing is the name of the library, which you specify here as library.

 

So far in the examples you have used Create React App, which in turn uses Webpack as a bundler. However, libraries often use Rollup as a bundler. One reason for this is that this tool is much more lightweight to configure than Webpack. For the build process of your library, you must first install several packages. To do this, you need to enter the command in this listing:

 

npm install rollup \n

   @rollup/plugin-commonjs \n

   @rollup/plugin-node-resolve \n

   @rollup/plugin-typescript \n

   rollup-plugin-dts \n

   rollup-plugin-peer-deps-external \n

   rollup-plugin-terser

 

The following table provides a brief explanation of each package you have just installed.

 

Dependencies for the Build Process

 

After installing these packages, the next step is to configure Rollup. You save this configuration in a file named rollup.config.js. You can see the contents of this file for the library here:

 

import commonjs from '@rollup/plugin-commonjs';

import resolve from '@rollup/plugin-node-resolve';

import typescript from '@rollup/plugin-typescript';

import dts from 'rollup-plugin-dts';

import peerDepsExternal from 'rollup-plugin-peer-deps-external';

import { terser } from 'rollup-plugin-terser';

 

const config = [

   {

       input: 'src/index.ts',

       output: [

           {

               file: 'dist/cjs/index.js',

               format: 'cjs',

               sourcemap: true,

           },

           {

               file: 'dist/esm/index.js',

               format: 'esm',

               sourcemap: true,

           },

       ],

       plugins: [

           resolve(),

           commonjs(),

           typescript({ tsconfig: './tsconfig.json' }),

           peerDepsExternal(),

           terser(),

       ],

   },

   {

       input: 'dist/esm/types/index.d.ts',

       output: [{ file: 'dist/index.d.ts', format: 'es' }],

       plugins: [dts()],

   },

];

 

export default config;

 

The Rollup config array contains two objects. The first object is responsible for bundling the library itself, and the second object takes care of generating the type definitions. You use the input property to specify the entry point to your library. For the example, this is the index.ts file in the src directory. As output, you define an array with two objects. One of them represents the library in CommonJS format, while the other represents the ECMAScript module system. In both cases, you have source maps generated for better debugging.

 

Finally, you use the plugin property to define which plugins Rollup should apply to your library's source code. On the one hand, these are the resolve and commonjs functions, which come from the @rollup/plugin-node-resolve and @rollup/plugin-commonjs packages, and on the other hand, you use the TypeScript plugin so that you can implement your library in TypeScript. The last two plugins, peerDepsExternal and terser, ensure that the bundle size is optimized.

 

With the TypeScript configuration, you’re still missing an important building block in the setup of your library. You can generate this file using the npx tsc --init command. You’ll then need to modify the default file to suit the requirements of your library. The result is shown here:

 

{

   "compilerOptions": {

       "target": "es2016",

       "esModuleInterop": true,

       "forceConsistentCasingInFileNames": true,

       "strict": true,

       "skipLibCheck": true,

       "module": "ESNext",

       "jsx": "react-jsx",

       "declaration": true,

       "declarationDir": "types",

       "sourceMap": true,

       "outDir": "dist",

       "moduleResolution": "node",

       "emitDeclarationOnly": true

   }

}

 

The Structure of the Library

Like a regular application, React doesn’t give you any guidance on the structure in the file system when building a library. You should organize your components in a directory structure that ensures that developers can quickly find their way around and easily locate the individual components of the library.

 

For this purpose, you first create a directory named src in the root directory of your application, where you store the elements separately from the library configuration. Then you can differentiate by the type of structure and create directories such as components and hooks.

 

Inside the components directory, you create another directory named Button, where you implement the first component of your library. You save the source code of the Button component in a file named Button.tsx. The corresponding source code is shown in this listing:

 

import React, { MouseEvent } from 'react';

import { StyledButton } from './Button.style';

 

type Props = {

   children: string;

   onClick: (event: MouseEvent<HTMLButtonElement>) => void;

};

 

const Button: React.FC<Props> = ({ children, onClick }) => {

   return <StyledButton onClick={onClick}>{children}</StyledButton>;

};

 

export default Button;

 

For your Button component, you first define the structure of the props that are expected by the component. Specifically, these are the children and the onClick props. The children prop stands for the label of the button. You can pass this label either via the explicit children prop or you pass the contents between the opening and closing component tags. The onClick prop receives the click handler, which is the function that’s executed when the button is interacted with.

 

The implementation of the button consists of the StyledButton component, to which you apply the two props you specified previously. To style the Button component, you use Emotion or the @emotion/styled package to create the StyledButton component, whose source code you store in the Button.style.tsx file in the Button directory:

 

Import styled from '@emotion/styled';

 

export const StyledButton = styled.button`

   padding: 5px;

   border-radius: 5px;

   background-color: lightgray;

   &:hover {

       background-color: white;

   }

`;

 

By creating the StyledButton, you’ve created the basic structure of the component. Then you assign multiple styles to the StyledButton component and define a hover state.

 

You can become as generic as you like, both in the button implementation itself and in the styling. With component libraries, you typically define generic components, whose appearance and behavior you can control using props, and which you can use in multiple places in your application or in multiple applications. The Button component in this example is deliberately kept simple, as the focus here is on creating the library itself and integrating the components into an application.

 

To be able to use the components of your library comfortably, you export the individual components of the library up to the central index.ts file. This has the advantage for users that they don’t need to know the internal structure of the library. For this purpose, you create one index.ts file per subdirectory that takes care of the export. The index.ts file in the Button directory is the first one:

 

export { default } from './Button';

 

The next level is then the index.ts file in the components directory. It collects the exports of all component directories. So if you implement additional components, you must add more entries in this file for the respective components:

 

export { default as Button } from './Button';

 

The final step consists of implementing the index.ts file in the src directory. This file exports all the structures of your library to a central location, which ensures that you don't need to memorize paths when integrating, but can import the structures directly. This file is thus the entry point into your application.

 

export * from './components';

 

In such a library, you can collect not only components, but also other structures like hooks or general helper functions that you can use in your application.

 

Hooks in the Library

One example of a hook you can implement in your library is a function that takes care of loading data. This hook function is named useLoadData and is located in a file of the same name in the src/hooks/useLoadData directory. The implementation of this hook function is shown here:

 

import { useState, useEffect } from 'react';

 

function useLoadData<T>(url: string): T[] {

   const [state, setState] = useState<T[]>([]);

 

   useEffect(() => {

       fetch(url)

          .then((response) => response.json())

           .then((data) => setState(data));

   }, []);

 

   return state;

}

 

export default useLoadData;

 

You implement the useLoadData function as a generic function in TypeScript. This means that when you call it, you can determine the types of the objects you load from the server. As a parameter, you pass the URL of the endpoint from which the data will be loaded. The return value is an array of objects of the generic type.

 

As with the component, you must export the hook function up to the index.ts file in the src directory. It starts with the index.ts file in the src/hooks/useLoadData directory:

 

export { default } from './useLoadData';

 

In the next step, you export the hook function in the hooks directory, as shown in this listing:

 

export { default as useLoadData } from './useLoadData';

 

In the final step, you export the hook function in the index.ts file in the src directory:

 

export * from './components';

export * from './hooks';

 

At this point, you’ve prepared the library to the point where you can build it.

 

Building the Library

When you initialize your library, you’ve already done most of the preparation for building the library. When you run the Rollup bundler with the current configuration, your library gets built in the dist directory and you can start using it. You can simplify the process a little more by adding a new script to your package.json file. Before editing the file, you need to use the npm install rimraf command to install an additional package that allows you to delete files and directories. The updated source code of the package. json file is shown here:

 

{

   "name": "library",

   "version": "1.0.0",

   "description": "Utility library",

   "scripts": {

       "prebuild": "rimraf dist",

       "build": "rollup -c"

   },

   "license": "ISC",

   "dependencies": {

          "@rollup/plugin-commonjs": "^22.0.0",

           "@rollup/plugin-node-resolve": "^13.3.0",

         "@rollup/plugin-typescript": "^8.3.2",

          "rimraf": "^3.0.2",

         "rollup": "^2.74.1",

          "rollup-plugin-dts": "^4.2.2",

          "rollup-plugin-peer-deps-external": "^2.2.4",

          "rollup-plugin-terser": "^7.0.2",

          "@types/react": "^18.0.9",

      },

     "peerDependencies": {

   "@emotion/react": "^11.9.0",

       "@emotion/styled": "^11.8.1"

       "react": "^18.1.0",

   },

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

   "types": "dist/index.d.ts"

}

 

To ensure that the build process runs without errors, you need to install the type definitions for React and Emotion via the npm install @types/react @emotion/react @emotion/styled command. In addition to these dependencies, you can also add React as a peer dependency (with peerDependency) in the package.json file to indicate that your library cannot work without installing React in the application. Furthermore, you can also specify the Emotion packages with peerDependency to make sure that they are also installed by the application and not indirectly by the library.

 

In a final step, you specify the entry point to your application with the main field and the reference to the type definitions with the types field.

 

At this state of your library, you can build it on the command line. For this purpose, you need to go to the root directory of the library and call the npm run build command. The result should be two success messages. Rollup reports that it has created the dist/cjs/index.js and dist/esm/index.js files on the one hand, and the type definitions under dist/index.d.js on the other.

 

Editor’s note: This post has been adapted from a section of the book React: The Comprehensive Guide by Sebastian Springer.

Recommendation

React
React

React.js makes developing dynamic user interfaces faster and easier than ever. Learn how to get the most out of the library with this comprehensive guide! Start with the basics: what React is and how it works. Then follow practical code examples to build an application, from styling with CSS to maximizing app performance. Whether you’re new to JavaScript or you’re an advanced developer, you’ll find everything you need to build your frontend with React!

Learn More
Rheinwerk Computing
by Rheinwerk Computing

Rheinwerk Computing is an imprint of Rheinwerk Publishing and publishes books by leading experts in the fields of programming, administration, security, analytics, and more.

Comments