DEV Community

Cover image for Create a React component library with Vite and Typescript
Nicolas Erny
Nicolas Erny

Posted on

Create a React component library with Vite and Typescript

Most of the time, we use our favorite tool to create a React application: create react app, next, gatsby...
But, it's a different story when it comes to building a component library. The choice is not straightforward. This article will show how to create a library with Vite and Typescript.

Why Vite?

Vite is a modern frontend tooling with excellent performance. You can get more details here. Out of the box, it supports typescript and library bundles. Therefore, it's a perfect choice to create a React library.

How to structure and organize our project?

Let's start creating a monorepo. We use yarn workspaces to manage dependencies.
To configure our monorepo, we need to create a package.json file at the repository's root.



{
  "name": "lib-example",
  "private": true,
  "workspaces": {
    "packages": [
      "packages/*",
      "sites/*"
    ]
  },  
}


Enter fullscreen mode Exit fullscreen mode

The repository has two folders:

  • packages containing the component library package
  • sites containing the site to test the library

Here's the tree structure.



react-library-vite-example
|- packages
|  |- my-lib
|- sites
|  |- my-site
|- package.json
|- yarn.lock


Enter fullscreen mode Exit fullscreen mode

Library package

Inside the packages folder, let's create a new Vite project:



yarn create vite my-lib --template react-ts


Enter fullscreen mode Exit fullscreen mode

By default, it creates a React web app configured with typescript. Now, we have to customize it to use the library mode from Vite.

First, we have to install a vite plugin to help us generate the type definitions for our components.



yarn add --dev vite-plugin-dts


Enter fullscreen mode Exit fullscreen mode

To bundle the library, we need to update the vite.config.js file.



import react from '@vitejs/plugin-react';
import path from 'node:path';
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';

export default defineConfig({
    plugins: [
        react(),
        dts({
            insertTypesEntry: true,
        }),
    ],
    build: {
        lib: {
            entry: path.resolve(__dirname, 'src/lib/index.ts'),
            name: 'MyLib',
            formats: ['es', 'umd'],
            fileName: (format) => `my-lib.${format}.js`,
        },
        rollupOptions: {
            external: ['react', 'react-dom', 'styled-components'],
            output: {
                globals: {
                    react: 'React',
                    'react-dom': 'ReactDOM',
                    'styled-components': 'styled',
                },
            },
        },
    },
});


Enter fullscreen mode Exit fullscreen mode

Notice that it's also important to externalize any dependencies you do not want to bundle into your library: react, react-dom, and styled-components.
Our rollup configuration generates two bundle formats: es and umd.

We add the following button component (MyButton.tsx) to our library as an example.



import styled from 'styled-components';

const MyButton = styled.button`
    border: none;
    border-radius: 0.5rem;
    background-color: #186faf;
    color: hsl(0deg, 0%, 98%);
    padding: 0.75rem;
    cursor: pointer;
    &:hover {
        background-color: #0a558c;
    }
    &:focus {
        outline: none;
        box-shadow: 0 0 0 2px #62b0e8;
        background-color: #0a558c;
    }
`;

export default MyButton;


Enter fullscreen mode Exit fullscreen mode

All the public React components are exported in the file src/lib/index.ts.



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


Enter fullscreen mode Exit fullscreen mode

Here's the updated package.json for our library:



{
    "name": "my-lib",
    "version": "0.0.0",
    "scripts": {
        "dev": "vite",
        "build": "tsc && vite build",
        "preview": "vite preview"       
    },
    "dependencies": {
        "react": "^17.0.2",
        "react-dom": "^17.0.2",
        "styled-components": "^5.3.3"
    },
    "devDependencies": {
        "@babel/core": "^7.16.12",
        "@types/node": "^17.0.12",
        "@types/react": "^17.0.38",
        "@types/react-dom": "^17.0.11",
        "@types/styled-components": "^5.1.21",
        "@vitejs/plugin-react": "^1.1.4",
        "acorn-jsx": "^5.3.2",
        "babel-loader": "^8.2.3",
        "typescript": "^4.5.5",
        "vite": "^2.7.13",
        "vite-plugin-dts": "^0.9.9"
    },
    "license": "UNLICENSED",
    "peerDependencies": {
        "react": "^16.8.0 || 17.x",
        "react-dom": "^16.8.0 || 17.x",
        "styled-components": "^5.0.0"
    },
    "files": [
        "dist"
    ],
    "main": "./dist/my-lib.umd.js",
    "module": "./dist/my-lib.es.js",
    "types": "./dist/index.d.ts",
    "exports": {
        ".": {
            "import": "./dist/my-lib.es.js",
            "require": "./dist/my-lib.umd.js"
        }
    }
}



Enter fullscreen mode Exit fullscreen mode

Run yarn build to compile the library.

As we bundle the dependencies into the library (except for the externals), we have to clean up the package.json of the published npm package. We do this by adding a prepack script.



"prepack": "json -f package.json -I -e \"delete this.devDependencies; delete this.dependencies\"",


Enter fullscreen mode Exit fullscreen mode

I use a CLI for working with JSON (yarn add -D json).

Website to test the component library

Let's start by creating a new Vite project in the sites folder.



yarn create vite my-site --template react-ts


Enter fullscreen mode Exit fullscreen mode

We need to add the following dependency to our package.json file to test our component library:



"dependencies": {
   "my-lib": "*",
   ...
},


Enter fullscreen mode Exit fullscreen mode

Now, we can reference and use our button component.



import { MyButton } from 'my-lib';

function App() {    
    return (
        <div className="App">
            ...
                    <MyButton onClick={...}>Click here!</MyButton>
            ...                
        </div>
    );
}

export default App;


Enter fullscreen mode Exit fullscreen mode

Run yarn install and yarn run dev to start the dev server.

Configure storybook

We also want to create documentation for our UI components. Storybook is a fantastic project to help us create a playground for our React components.

Run the following command to configure Storybook:



cd /packages/my-lib && npx sb init --builder storybook-builder-vite


Enter fullscreen mode Exit fullscreen mode

At the time of writing, the interactions addon does not work well with Vite. Here's the customized configuration (.storybook/main.js):



module.exports = {
  stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-interactions",
  ],
  framework: "@storybook/react",
  core: {
    builder: "storybook-builder-vite",
  },
};


Enter fullscreen mode Exit fullscreen mode

Finally, we create a story file for our button component.



import { ComponentMeta, ComponentStoryObj } from '@storybook/react';
import MyButton from './MyButton';

const meta: ComponentMeta<typeof MyButton> = {
    title: 'Design System/MyButton',
    component: MyButton,
};
export default meta;

export const Primary: ComponentStoryObj<typeof MyButton> = {
    args: {
        disabled: false,
        children: 'Hello',
    },
};


Enter fullscreen mode Exit fullscreen mode

Run yarn run storybook to start the storybook.

Library storybook

If you want to learn more about Storybook, check out the official documentation.

What's next?

We've just created an excellent Vite startup project. But, we can go further and configure additional tools such as eslint, prettier, jest...

You can find the source code on Github.
This has been helpful for me in my projects. Hopefully it helps you as well.

Top comments (44)

Collapse
 
njavilas2015 profile image
njavilas2015 • Edited

good evening your article fell from the sky, I would like to share an improvement

import react from '@vitejs/plugin-react';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { defineConfig, UserConfigExport } from 'vite';
import dts from 'vite-plugin-dts';

const App = async (): Promise<UserConfigExport> => {

    var name: string = 'replaceme';

    const data: string = await readFile(path.join(__dirname, 'src', 'lib', 'index.tsx'), { encoding: 'utf-8' })

    const s = data.split('\n')

    for (let x of s.reverse()) if (x.includes('export default')) name = x.replace('export default ', '').replace(" ", "")

    return defineConfig({
        plugins: [
            react(),
            dts({
                insertTypesEntry: true,
            }),
        ],
        build: {
            lib: {
                entry: path.resolve(__dirname, 'src/lib/index.tsx'),
                name,
                formats: ['es', 'umd'],
                fileName: (format) => `lib.${format}.js`,
            },
            rollupOptions: {
                external: ['react', 'react-dom', 'styled-components'],
                output: {
                    globals: {
                        react: 'React',
                        'react-dom': 'ReactDOM',
                        'styled-components': 'styled',
                    },
                },
            },
        },
    });
}

export default App 

Enter fullscreen mode Exit fullscreen mode

Image description

This improvement takes the name of the component that we are exporting, so that it is dynamic to obtain the name to import it into our projects

and it works great!

Image description

I took the liberty of exporting our code as lib by default, so I didn't have to keep renaming the files.

Image description

Collapse
 
nicolaserny profile image
Nicolas Erny

Nice improvements! Thanks :)

Collapse
 
stephyswe profile image
Stephanie

maybe a repo link? i tried your solution but Im not sure how to, or what it suppose to do?

Collapse
 
russmax783 profile image
Russ

Thanks for this! Was finally able to create a shared component library in my project. Been meaning to do that for a while. As of the time of this comment, to get storybook working I had to add:

<script>
  window.global = window
</script>
Enter fullscreen mode Exit fullscreen mode

to a file called .storybook/preview-head.html

Collapse
 
nicolaserny profile image
Nicolas Erny

Good to know! Thanks :)

Collapse
 
neolivz profile image
Jishnu Viswanath

After considerable amount of time spent on migrating component library build from rollup to vite I realised that vite build is slower than rollup

All timing values are taken from vite-plugin-time-reporter plugin for both vite and rollup

First vite report
dev-to-uploads.s3.amazonaws.com/up...

On update with watch
dev-to-uploads.s3.amazonaws.com/up...

With rollup
dev-to-uploads.s3.amazonaws.com/up...

It looks like vite is good for only single page webapp build not for component library on mono repo

Collapse
 
nicolaserny profile image
Nicolas Erny

Thank you for your feedback. I don't have much experience with rollup. But it's good to know that rollup can be a more performant choice. On my projets, I'm happy with performance, but I don't have any comparison with rollup...
I wonder if there is a bug in vite. I will check out the issues on Github.

Collapse
 
jurito profile image
jurito • Edited

GREAT article!
Just a question. With this approach, how would I implement a library with folders?

My target would be being able to do smth like:

import { component } from 'mylibrary/folder';
import { othercomponent } from 'mylibrary/otherfolder';

Enter fullscreen mode Exit fullscreen mode
Collapse
 
nicolaserny profile image
Nicolas Erny

Thanks 🙏
I think I found the solution in the vitejs documentation: vitejs.dev/guide/build.html#librar...
You can define multiple entry points in the Vite config and expose them with folders (package.json).

Collapse
 
palmerama profile image
Adam Palmer

Great article! Thanks. But your prepack script deletes all my deps on npm publish. Is there a way to make it add them back in?

Collapse
 
nicolaserny profile image
Nicolas Erny • Edited

For my use case, it wasn't an issue because I only run pack/publish in our CI environment.
If you want to get the deps back, here's a quick idea:

  • create a temporary backup of the package.json file in the prepack script
  • add a postpack script to restore the package.json file

Perhaps, we could find a tool to manage the packaging/versioning. For more advanced use cases, I would start digging here: turbo.build/repo/docs/handbook/pub....

Hopefully, my answer will be useful.

Collapse
 
palmerama profile image
Adam Palmer

Thanks Nicolas. I've actually just implemented this, which solves it nicely. Removes and adds back in the deps, along with anything else the package doesn't need, like scripts. npmjs.com/package/clean-package

Thread Thread
 
stephyswe profile image
Stephanie

any way to share repo? :) @palmerama

Collapse
 
yazankhatib profile image
Yazan Alkhatib

I'm trying to deploy the app which I'm using to import and test my package, it's building alright in development but when trying to deploy to Vercel or Netlify I'm getting the following error:

src/App.tsx(2,22): error TS2307: Cannot find module 'yaa-grid' or its corresponding type declarations.
error Command failed with exit code 2.

I'm expecting that it's something that has to do with the types file

Collapse
 
arthurseredaa profile image
Hey! I'm Arthur

I have same issue but in development(

Collapse
 
nicolaserny profile image
Nicolas Erny

I will investigate more when I have some time.
Perhaps, we need to upgrade some dependencies...

Collapse
 
rafaelnogueira profile image
Rafael Nogueira

Thanks for this article, it was very helpfull.
I have a problem when use the component in my project. It show: TypeError: s.button is not a function
It seems to happen in one of the modules

Call Stack
<unknown>
dist/vfb-components.es.js
Enter fullscreen mode Exit fullscreen mode

It looks like a problem with the style file or with the imported styled from styled-components.
This only happens when a install the library at NextJS project. It works fine with CRA.

Any ideia how to fix this?

Collapse
 
rafaelnogueira profile image
Rafael Nogueira

So i found a topic about this problem. It only happens with projects using SSR.
There's no official fix for this at the moment, but this fix works github.com/styled-components/style...

Collapse
 
nicolaserny profile image
Nicolas Erny

Thanks for sharing.

Collapse
 
nicolaserny profile image
Nicolas Erny

Hard to investigate. I use this strategy with Nextjs on some projects without any trouble: component library with Vite + webapp in Nextjs. Do you have a public github repository with this issue?

Collapse
 
mohamedabusrea profile image
Mohamed Abusrea • Edited

Thank you for the great article. I'm facing an issue when adding an extenral library like antd or like szhsin/react-menu. I get this error:

TypeError: Cannot read properties of null (reading 'useState')
    at Object.useState (http://localhost:6006/@fs/Users/mohamedabusrea/Desktop/Workspace/component-library/dist/component-library.es.js?t=1665733637289:1628:29)
    at useTransition2 (http://localhost:6006/@fs/Users/mohamedabusrea/Desktop/Workspace/component-library/dist/component-library.es.js?t=1665733637289:36830:33)
    at useMenuState2 (http://localhost:6006/@fs/Users/mohamedabusrea/Desktop/Workspace/component-library/dist/component-library.es.js?t=1665733637289:36898:24)
    at useMenuStateAndFocus2 (http://localhost:6006/@fs/Users/mohamedabusrea/Desktop/Workspace/component-library/dist/component-library.es.js?t=1665733637289:36911:23)
    at Menu2 (http://localhost:6006/@fs/Users/mohamedabusrea/Desktop/Workspace/component-library/dist/component-library.es.js?t=1665733637289:36937:31)
    at renderWithHooks (http://localhost:6006/node_modules/.vite-storybook/deps/chunk-5LLNUTNH.js?v=f9c3babb:11763:26)
    at updateForwardRef (http://localhost:6006/node_modules/.vite-storybook/deps/chunk-5LLNUTNH.js?v=f9c3babb:13907:28)
    at beginWork (http://localhost:6006/node_modules/.vite-storybook/deps/chunk-5LLNUTNH.js?v=f9c3babb:15479:22)
    at beginWork$1 (http://localhost:6006/node_modules/.vite-storybook/deps/chunk-5LLNUTNH.js?v=f9c3babb:19248:22)
    at performUnitOfWork (http://localhost:6006/node_modules/.vite-storybook/deps/chunk-5LLNUTNH.js?v=f9c3babb:18693:20)
Enter fullscreen mode Exit fullscreen mode

any clue?

Collapse
 
nicolaserny profile image
Nicolas Erny

Hello, I've just tried to use react-menu without any trouble. I wonder if you have conflicts between multiple React versions locally. You can check with "yarn why react".

Collapse
 
mohamedalkhawam profile image
mohamedalkhawam

Hi Thanks for your article, but I do have the same issue

Uncaught TypeError: Cannot read properties of null (reading 'useState')
at Object.useState

I here is my "yarn why react".

info Has been hoisted to "react"
info This module exists because it's specified in "devDependencies".
info Disk size without dependencies: "388KB"
info Disk size with unique dependencies: "420KB"
info Disk size with transitive dependencies: "448KB"
info Number of shared dependencies: 2

Collapse
 
giladl82 profile image
Gilad Lev-Ari

Hi, thanks for the explanation.
I have a small question.
I created a library based on this post, and when trying to use it inside my app, I get no IntelliSense on the component's props.

How do I need to set my d.ts?

Collapse
 
felixselter profile image
FelixSelter • Edited

Change your libraries package.json from:

"types": "./dist/index.d.ts",
"exports": {
    ".": {
      "import": "./dist/lib.es.js",
      "require": "./dist/lib.umd.js",   
    }
  }
Enter fullscreen mode Exit fullscreen mode

to:

"exports": {
    ".": {
      "import": "./dist/lib.es.js",
      "require": "./dist/lib.umd.js",   
      "types": "./dist/index.d.ts",
    }
  }
Enter fullscreen mode Exit fullscreen mode

It seems like the type field is ignored when theres an export field:
stackoverflow.com/a/76212193/11355399

Collapse
 
nicolaserny profile image
Nicolas Erny

Hi :) It should generate the types. 🤔
In the vite.config.ts, I use this lib to generate types:
import dts from 'vite-plugin-dts';

plugins: [
react(),
dts({
insertTypesEntry: true,
}),
],

Collapse
 
cburrows87 profile image
Chris Burrows

Hi, I keep running into the issue of "require undefined"
Image description

This is from a straight pull down from you repository as well as when I do it by hand. Have you encountered this problem before? I have scoured the internet, but can't seem to find a solution :|

Any help would be great!

Collapse
 
nicolaserny profile image
Nicolas Erny

It looks like there is something messy in the node_modules (especially with the alpha version). I've just pushed an update to use fixed dependencies. Now, it should work smoothly with node 16.
For sure, there is work to do if we want to update the dependencies (at the time of the article, only the alpha version was available). I will give it a try when I have more time. Hope it helps.

Collapse
 
nicolaserny profile image
Nicolas Erny

Another idea: if you want to use the latest version of storybook (instead of the alpha versions), you have to use react 18. I remember that I had issues (in another project) with react dom client and React 17. Hope it helps.

Collapse
 
cburrows87 profile image
Chris Burrows

Awesome, I will give it a go! Thanks for the swift reply! I've been trying to just get a component library ready and your approach seemed great! (Once i got it working XD)