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/*"
]
},
}
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
Library package
Inside the packages folder, let's create a new Vite project:
yarn create vite my-lib --template react-ts
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
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',
},
},
},
},
});
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;
All the public React components are exported in the file src/lib/index.ts
.
export { default as MyButton } from './MyButton';
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"
}
}
}
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\"",
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
We need to add the following dependency to our package.json file to test our component library:
"dependencies": {
"my-lib": "*",
...
},
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;
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
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",
},
};
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',
},
};
Run yarn run storybook to start the 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.
Discussion (10)
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:
to a file called
.storybook/preview-head.html
Good to know! Thanks :)
@nicolaserny Salut ;) sympa ton article
As-tu remarqué que tu as une dépendance lorsque tu fais l'install dans
my-site
Je ne pense pas que le my-lib que tu importes soit le bon.
D'ailleurs dans ton
app.tsx
ou tu utilisesMyButton
il y à une erreur TypescriptModule '"my-lib"' has no exported member 'MyButton'.ts(2305)
J'ai essayé en changeant le nom de la library mais dés que j'essaye de lancer un install j'ai une 404 yarn/npm/pnpm tente de chercher la library sur npmjs =/
Salut :) Je viens de retester le code localement mais je ne vois pas de problèmes. Du coup je suis curieux. J'utilise node 17 (mais j'ai testé aussi avec node 16) + yarn 1.22.19 (il faut que la version de yarn supporte les workspaces)
Comme on utilise des workspaces, yarn va gérer la dépendance avec la lib (my-lib) via un lien symbolique.
Normalement je fais un yarn install (tu peux le faire à la racine ou dans my-site, ça fait la même chose). Après il faut build la library avec yarn build (vu que le site de test va pointer directement sur le build). et normalement dans my-site tu n'as pas de problème (yarn dev pour tester). Je ne trouve pas de version 2.0.1 (tu vois ça à quel endroit ?) Je n'ai pas d'erreur typescript (tu as bien compilé la librairy?)
Voilà en tout cas les pistes pour te débloquer.
J'utilise ce fonctionnement en monorepo sur plusieurs projets au travail.
Si tu as d'autres questions n'hésite pas.
Hum ok je vais tester de nouveau
J’ai eut l’erreur en utilisant pnpm pour le coup y’a sûrement un écart entre yarn et pnpm
Après j’ai utilisé la fonctionnalité Workspace de pnpm qui marche bien :)
C’était une approche de monorepo que je n’avais pas encore testé
Petite question d’ailleurs comment tu gères les déploiements et versioning de chaque package ? (En gros est ce que tu peux faire un truc similaire à ce que propose lerna ?)
Grosso modo le cas d’utilisation :
Si tu développe dans le packageA et que tu le déploi le packageB ne devrait pas être déployé
Merci pour ta réponse ;)
En effet, je ne suis pas sûr que les notions de workspace soient les mêmes entre yarn et pnpm (j'utilise surtout yarn).
Je n'ai jamais utilisé lerna (donc je ne pourrai pas comparer). yarn workspace a la capacité de remplacer les wildcards par les vrais versions (lors d'un publish). donc c'est assez flexible je pense. Mais je n'ai pas encore eu besoin de creuser la question.
Actuellement j'ai tendance à mettre la même version sur tous mes packages du mono repo (lors d'une release en CI). ça me simplifie la vie. Si certains packages sont des libs utilisées par d'autres projets alors je publie dans npm. si c'est une webapp alors je package avec docker (mais c'est un autre sujet)...
Le monorepo a des avantages et des inconvénients: comme tout est dans le même repo, la CI va forcément builder tout le repository donc en terme de déploiement si tu veux déployer uniquement A et pas B c'est plus compliqué (je ne pense pas qu'on puisse faire ça automatiquement). Dans ce cas, je ferais des chaines de builds spécifiques. Par contre le monorepo est agréable pour la partie dev. Tout est question de tradeoffs.
Thanks for this tutorial. It has been very helpful for me.
I'm really new to Vite and I'm really trying to give it the benefit of the doubt. I'm having an issue with a dependency (xml-parse) that uses __dirname. According to the Vite docs, this reference should be replaced, but it is ending up in my built code and causing errors in the browser (namely __dirname is not defined). I've tried multiple paths to figure this out but I seem to just be heading down one wrong path after another. Any advice for how to resolve this?
Hello :)
I never experienced this error.
Which OS, and node version do you use?
With this project structure i have to run build every time that i have a change in my library no? how can i solve that
True, you have to rebuild the library to see the changes in the example. Most of the time, it's not an issue because you use the storybook (with a devserver) while you do intensive developments in the library.
However, if you want to rebuild automatically the library, I suggest to check out this tool: nodemon.io/