
If you are managing multiple React applications and want consistency across your user interfaces, sooner or later you'll find that you need a compo...
For further actions, you may consider blocking this person and/or reporting abuse
Hi Vesa,
this is very interesting, thank you! I did not yet try to use this approach within a Next.js project.
You can also remove the inject-css plugin (and the
sideEffects
from thepackage.json
)Having done that you should be able to just import the generated css file (
dist/assets/style.css
) inside your Next.js project. But of course you will not have the css treeshaking advantages with this approach.I published a branch with this approach here: github.com/receter/my-component-li...
Generally there are answers for the quetions raised in the linked issue:
As Global CSS.
The order of the individual files is determined by the order they are imported inside the libraries main file.
And the order in the consuming application should not matter.Maybe it would be possible to write a Next.js plugin enabling this.
What do you think?
The guide discusses essential technical details, such as TypeScript integration, CSS modules, tree shaking, and peer dependencies. It covers these aspects while keeping the content concise and understandable.
Andreas, thank you for sharing such a well thought out and detailed explanation of how to set up a component library. This subject isn't easy, but you did a great job explaining everything. I took inspiration from your setup and expanded it to include support for
esm
andcjs
bundles in addition to subpath exports setup for explicit import references for anyone interested. You can find it here.Awesome, thanks for sharing!
Not sure why you want a cjs version that imports css files. Do you have a use case where a bundler can import css files but not esm? If not it wouldn't make sense to me.
Good question, this was more about compatibility between our legacy and newer codebases. The goal was to implement a bundler config that would operate for both situations so that when we do migrate to a fully esm system, the conversion would be painless. Probably not necessary in most cases considering cjs doesn’t gain any tree shaking benefits typically, but an activity I enjoyed doing to see if I could get both esm and cjs bundle outputs working.
Great article really helpfull! 🥳
Keep up the awesome work 💪!
But my .stories files gets included in the bundle.
I tried to exluded them by extending the glob.sync:
glob.sync('stories/**/*{!(*.stories).ts,!(*.stories).tsx,!(*.stories).js,!(*.stories).jsx}')
But it did not work.. 🤔
Any idea why this is happening?
Thx!
Edit:
Fixed it, I was stupid and included the * in the begining:
glob.sync('stories/**/{!(*.stories).ts,!(*.stories).tsx,!(*.stories).js,!(*.stories).jsx}')
But it still includes the stories.d.ts... 😞
Edit 2:
Sorry I'm asking to fast 😅, working glob that exclude stories files:
'stories/**/{!(*.stories|*.stories.d).ts,!(*.stories).tsx,!(*.stories).js,!(*.stories).jsx}'
Edit 3:
I initially thought it worked. But stories.d.ts still included.. :/
I was successful with this:
glob.sync('lib/**/*.{ts,tsx}', { ignore: 'lib/**/*.stories.tsx'})
Thx man!
That worked great 🤩!
I have added a FAQ section about using Storybook: dev.to/receter/how-to-create-a-rea...
Hi Js4,
why do you have your stories in the library? I think it would be better to place them outside the
lib
folder. Then you shouldn't have to exclude them.Hy thank you so much for the guide. I do have a question though.
if in my library i'd like to achieve an import like: d
how can i do it.
do i have to pass an array of entries like?
my structors is
lib/assest
lib/hooks
etc
So sorry for the dumb question
Hi Federico,
You should be able to do that with subpath exports.
First create a file
assets.ts
alongsidemain.ts
and export what you want:Then in
package.json
you can define the exports like so:You can read more about this here: nodejs.org/api/packages.html
Let me know if this works for you.
For multiple entry points like this, do we need to update vite config lib.entry as well as the rollupOptions.output?
Yes, either that or you could also do multiple build steps. Like a different vite config file for each export and then run
vite --config ./vite-build-main.config.ts build && vite --config ./vite-build-assets.config.ts build
for example.Thank you so much @receter. I do have another questions. I'm using this set(more or less) for my RN lib.
I having trouble with react-native-reanimated library. I created my animated Comp in the library and set reanimated as a external lib and peer, i can build without problems my lib but then when i try to import my comp the app crash. I don't get the problem to be honest. Do you have any suggest? thank you in advance
Hi Frederico,
do you get any kind of error message? If you can provide a repo where I can reproduce your issue, I am happy to help you find it.
That's my biggest problem, i don't get an error. the app just crash. i gonna create a simple repo and share it with you. Really appreciate your help
@receter i'll add here the link for the repo example
👍 Let me know when it is ready!
Hey, i added in the comment the link for the lib repo. do you need also a simple repo app example? Thank you so much
@receter Sorry for the ping but maybe you didn't get a notification
I did, will do that today.
Just finished writing a guide to automatically publish packages if your are interested. And I will now read the code in your repo 😉
@federbeije I opened an issue in your repo, we can continue to discuss there.
Thanks for the link!
The reason I am working on this topic and wrote the article is that I am trying to find the best solution to build a component library. I don't like CSS in JS that much and I am convinced that a stylesheet based approach is the way I want to go. I will think about the style ordering and might publish another article on this soon.
I wrote you on LinkedIn, if you are interested in having a discussion about this topic I would be more then happy to speak/write to you.
The advantage of handling the style imports inside the library is (obviously) that you don't need to manually import styles. This is not a big issue if it is just one stylesheet for a library. But if you only want to import the styles for components you actually use I see no other really satisfying solution.
I do have some rough ideas though…
Thank you for your work. It helped me to move from Rollup to Vite.
I just had to make some changes related to my implementation, but it worked with most of your lessons.
If you are interested, here is the link to the issue where I collected all the knowledge from this experience.
Cool, glad to hear it helped you.
And I am very interested in your component library and architecture, I will have a closer look. I already noticed you use Hygen for code generation, I didn't know about that but I need this :)
I'm also working on a component library at the moment, if you're interested in sharing thoughts and experiences I'd be up for it, maybe in Discord or something.
Thanks for sharing!
Thank you for your work and your words, Andreas. My Discord is
nicolasomar
if you want to talk sometime.I am glad to help in any endeavor you may need a hand.
GREAT post. I was struggling with this. I especially overlooked
react/jsx-runtime
as an external dep as was getting frustrated why it was being included in the build. So you saved me a fair bit of time there.The CSS splitting I would have figured out.... eventually. But after a lot of frustration and maybe hours of searching. So, again, big thanks. Followed.
Hi Ben, very glad I saved you some time and thanks for letting me know. That was the main reason for me to write this article. It took me some time and frustration to get it all working and I wanted to share my learnings so that others have a better starting point.
If you find anything that can be improved or needs to be updated let me know!
Hello!
I found and issue with the tsconfig files in the latest vite version (5.4 at the time of this comment). While previous versions of create vite would generate a tsconfig.json and a tsconfig.node.json, the latest version creates an additional tsconfig.app.json making it no longer possible to make the steps in this tutorial work by extending any of these files in the tsconfig-build.json. What solved the issue for me was deleting all three files and copying tsconfig.json and tsconfig.node.json from an older vite project. Hope this helps!
I created a new
tsconfig.build.json
file and extended tsconfig.app.json, seems to work fine:{
"extends": "./tsconfig.app.json",
"include": ["lib"]
}
Also, with the new vite version with tsconfig.app.json, for building type definitions I had to add tsconfigPath:
dts({ include: ['lib'], rollupTypes: true, tsconfigPath: 'tsconfig.app.json' }),
I would suggest this:
Do not include the
lib
folder intsconfig.app.json
, instead create atsconfig.lib.json
like so:Then add
tsconfig.lib.json
to thereferences
array in yourtsconfig.json
.And set the build script to:
I made a branch with the latest vite version (5.4.4 at the time of writing): github.com/receter/my-component-li...
Here is a summary of what I had to change for the building the types:
update to vite-plugin-dts@4
add tsconfig path to dts config
This outputs types to a
lib
subfolder in dist. So you need to update your package.json to point to the correct type entry: "./dist/lib/main.d.ts" (or you could use rollupTypes: true)Also I manually had to install
ajv
because of an issue: stackoverflow.com/questions/787352...In this branch I also use the new package.json
exports
feature and "self reference" the library in App.tsx.I will try to find time to update my article accordingly, but for now this branch might help you.
I just found out that the issue with
ajv
was because I (unintentionally) hadlegacy-peer-deps=true
in my global npm config on my other computer. I have updated the FAQs of this article with more details.I just created a new nextjs project and it seems to work quite fine.
Here is the my repo: github.com/receter/my-nextjs-compo...
What version of nextjs were you using? I would like to reproduce.
Also can you try if this works for you:
In
next.config.js
:Looks like it works since Next.js 13.4:
github.com/vercel/next.js/discussi...
github.com/vercel/next.js/discussi...
Hi Andreas,
thanks for excellent article and sample code! I followed your entire article and everything works perfectly then I tried npm link on the project root and ran npm link @username/my-component-library on a new project that uses the library. On this project I tried to debug the Button component (with devTools of Chrome) but I only see the compiled code (dist/components/Button index.js). Is it possible to view the source code in debug?
Thanks :)
You're welcome, I'm glad you like the article. First, if you run
npm run dev
you can start the dev server and debug your components locally. Further you can install something like react-cosmos or storybook locally to test and debug your components.If you really need/want to install your package with
npm link
to test it in another project you can try if building with a sourcemap solves your problem: vitejs.dev/config/build-options#bu...Let me know if that helps you, cheers!
Hi Andreas, thank you for this really useful post.

I wanted to comment that I am having an issue when compiling, on the dist folder, instead of getting "main.js", "main.d.ts", and "components", I am getting this structure:
Here, under "dist/components/Button" for example, I have the index.js with the compiled code. And under "dist/lib/components/Button" I have the index.d.ts with the declarations
So I have to do nested imports to get to the correct main.d.ts file and that isn't ideal.
Do you have any thoughts on what may be causing this problem? thanks in advance
It was due to me importing files from outside lib
good to know, glad you found the issue
Yes you are absolutely right about overrides. If you assign classes to the exported components it makes a difference if these classes come before or after the classes provided by the component library.
In this case the component library styles have to be imported before to have a lower specificity. And this is only guaranteed if the library is imported before any other styles.
As you correctly mention, the consuming application is responsible for ensuring the correct order of css.
You should be able to ensure this if:
Do you have more info like a github issue on this?
Can you ellaborate on this?
I have the feeling that a fundamental problem is that the order of CSS is defined by the order it is imported in javascript. Which is kind of by chance because sometimes a component is imported earlier and sometimes later.
There are also not so easy to fix problems with dynamic importing github.com/vitejs/vite/issues/3924
I created a branch with a very simple example that demonstrates the order issue if anyone is interested in an example: github.com/receter/my-component-li...
What I did not expect: When importing ANY component from the library, all other imported CSS, even for components imported later on, will be at the same position.
@merri If you are interested I have written an article on CSS order of appearance. And in the meantime I have started to use CSS Layers to prevent these issues.
Thank you, that really helped me ❤️
Creating and Publishing React Npm Packages simply using tsup medium.com/@sundargautam2022/creat...
Looks interesting, did you get any experience with TSUP and CSS code splitting? One of my requirements was that not all CSS is bundled, but only the CSS of components that are imported.
I have problem that the generated main.js has imports to 3rd party libs.
Generated main.js:
import { Button as i } from "./components/Button/Button.js";
import "react/jsx-runtime";
import "tailwind-variants";
import "@nextui-org/react";
export {
i as Button
};
But my main.ts looks like this:
export {Button} from './components/Button/Button'
I have all three of them in the exclude block:
external: ['react','react/jsx-runtime',"tailwind-variants","framer-motion",RegExp("^(@nextui-org/).+")],
Why do Vite add them to the final build?!
I had removed this plugin in the Vite.config:
import {libInjectCss} from 'vite-plugin-lib-inject-css'
Because I dont use css files I use Tailwind classes.
But when I add this lib back the imports are not added.. 🤯
I have no idea why.. but at least it works....
Interesting, I will investigate this. Do you have a public repo to reproduce?
Hi man!
I do not have a repo, this package is for internal use only.
But I created a sandbox for you to test with :). I tested and when you remove the libInjectCss from vite.config main.js gets the imports.. you run
pnpm build-sb
to build it andpnpm sb
to start the project.Sandbox Project
could u guys fix this issue?
Tailwind needs Postcss using Vite so add it like so to your vite.config.ts
Put it in the external and output of rollup based on your needs.
Thanks for this article, I followed each step and works perfectly!!
Is there a possibility to make this work for sveltekit? I am trying to adapt this to it, but it is not working as spected.
I would like to try Svelte… What did not work as expected?
Really great tutorial! Thanks!
I'd like to add StoryBook to the component library. Is this straight-forward or is there anything special I have to consider?
I used
npx storybook@latest init
to install Storybook, which seemed to work fine. I can start Storybook in dev mode withnpm run storybook
, howevernpm run build-storybook fails with an error:
`=> Failed to build the preview
TypeError: Cannot convert undefined or null to object
at Function.values (<anonymous>)
at configResolved (file:///C:/prj/react/cp-web-react-components/node_modules/vite-plugin-dts/dist/index.mjs:582:100)
I looks like it has to do with the modified vite setup...
Hi Peter,
The issue comes from using the plugin
libInjectCss
with Storybook. You should not need this plugin to build Storybook. You can remove it from the config when building Storybook.Add this to
.storybook/main.ts
add this:Hi Andreas,
that did it! Thanks a lot!
This is really a nice setup for our company component library!
Here is the branch with Storybook: github.com/receter/my-component-li...
Hi Peter, thanks for your comment. I will try to find some time over the weekend and create a branch with storybook.
Pls, write the new tutorial with vanilla-extract. I am looking for that one.
Here is a branch with vanilla-extract: github.com/receter/my-component-li...
I heard you :)
This was super helpful!
I'm still deciding between emotion or styled-components or regular sass but this had everything I needed to set up my vite component library
Thank you Andreas!
love this 🔥
Hi, Vesa,
I'm not having this problem with Next. The doc you provided says that this issue occurs when the source files are consumed instead of the build files. Are there other circumstances that produce this problem that we should watch out for?
My steps, in a monorepo with NPM workspaces:
packages
).apps
).page.tsx
. This file doesn't contain the 'use client' directive, so I believe it's SSR.Results:
Question:
Are there cases in which we would consume the build files and still encounter this issue?
I added a faq section about using Storybook: dev.to/receter/how-to-create-a-rea...
dev.to/receter/comment/2ag7m
When I import a component in another project, I get the this error with vitest when running tests:
I am facing same issue, anyone have solution??
how do exclude .test / .spec files from the output?
Figured it out. You can add this to dts:
and this to the rollupOptions:
Thanks, it helped me lot.
Thank you very much for this tutorial. I am using this to build a production app in my workplace, The issue I am going through is something like this.
Think that a button components is using types which are coming from icon component also.
So in the button component there will be a import like
import iconProps from "../icons/types"
but after building the project all the t.ds files are in root level but still the button.js build file has a import import iconProps from "../icons/types" which is not resolving since there is no icon folder inside the dist folder now. Seems like even I managed to add all the d.ts files in root level the js files using them are not resolving the path properly.
I am using multiple entry points for dts plugin as well.
So I managed to add d.ts files of some components on the relevant folder without ading them to root, which is not the ideal way I guess
Hi Lanka,
Thanks, glad you like the article. Could you create a repo with a minimal example to reproduce this? Can you ellaborate on why you need this config for dts? Why don't you exclude the story files in the rollup config?
And here is a branch on how I would recommend to setup storybook: github.com/receter/my-component-li...
Are you sure it is correct to use
peerDependencies
for react here? Based on the npm doc, the package is expected to not import a peerDependency.peerDependencies
should be read as: "dependencies if this project is a plugin". And a component plugs in to another project--it's not a project itself. So it relies on its host's plugins. peerDependencies says "as a plugin I want my host to have these deps" (which is why it's important to test with a variety of versions and be very lenient with the sem-version control).Yes, using
peerDependencies
for React is appropriate here.peerDependencies
should be used for packages that the consumer is expected to provide. This ensures that React is not automatically installed by the library, preventing multiple versions of React from existing in the same project, which is essential for React to work.use rollupTypes: true in your vite.config.ts and the typescript version 5.4.2 if you are doing this now. As rollupTypes will give you the required types file and the dts package work with the older typescript
`
import react from '@vitejs/plugin-react'
import { resolve } from 'path';
import dts from 'vite-plugin-dts'
// vitejs.dev/config/
export default defineConfig({
plugins: [
react(), dts({ include: ['lib'], rollupTypes: true })
],
build: {
copyPublicDir: false,
lib: {
entry: resolve(__dirname, 'lib/main.ts'),
formats: ['es']
}
}
})
`
So 3 questions.
Hi Bica,
Thanks for you comment!
copyDtsFiles
and all your d.ts files will get copied to the output.I also noticed that for each d.ts file an empty d.js file is created. This is not wanted and you can fix it by changing the glob to
lib/**/!(*.d).{ts,tsx}
:Here is a branch that uses global types: github.com/receter/my-component-li...
You need to export all components that you want to expose to the outside. If you have a component that is only used internally you don't have to include it in
main.ts
.But not all components will end up in you final application bundle, only the components that you import will.
To use SASS you need to first install it
And then you can just rename files to for example
styles.module.sass
and everything should compile fine.If you need a global stylesheet you can create a
lib/global.sass
and import it inmain.ts
:Here is a branch that uses global sass for the Button and a global stylesheet: github.com/receter/my-component-li...
Does this answer your questions sufficiently?
I had noticed my index.d.ts becoming converted to index.d.js and being empty.
On the export question, our setup differs a bit from the standard react component folder structure....i.e how I'm doing it is basically..
So in main.ts I guess I have to do
Yes? I get an error if I don't specify as default because I don't have my components as index.tsx files.
Then we are using Bootstrap. They haven't updated their sass to support @use and @forward, they still use @import so I have to be careful when pulling it in due to increased file size and duplication. What I was doing was running the sass command on build so a css was compiling and added to dist so the end developer could pull it in if they chose (from a theme perspective). However I want to play around with your solution inside the main.ts
Per the above I have to be careful adding a sass import to the top of individual components because if an input is used multiple times per page, I fear the reference to the styles will be repeated....I am not sure if that is valid in react. But if that file has to reference our bootstrap sass for their utility methods (i.e. color or fontsize) I think it will try to pull in bootstrap again due to the import issue I mentioned.
I want to have a playground locally to test the package but I don't want my local testing to be a part of our commit, just the package library.
For exporting your components you should be able to:
But inside of toggleWhatever.tsx you need to have a named export like:
If you for some reason need to export the components as default you can also do it like this which is easier to read in my opinion:
Not 100% sure what you mean with the SASS, but if you import a button multiple times the styles will be included just once.
CSS duplication issues can also be handled downstream by your bundler.
If you use a global CSS library you should be fine by just adding an import in
main.tsx
In this case the CSS library will end up in your application as soon as one component of the library is used. You could also add an import for the library to every component that needs the library, so the CSS library is only bundled if one of these components is imported.Yes it does! I'll check out your fixes and if they will work with my changes.
Thank you this is very helpful. I cloned your latest repo revision 1.
What I would like to do is create my own custom MUI based components for this library. I tried npm install @mui/material, added a new folder lib/components/MuiButton with an index.tsx of just a very simple example:
import Button from '@mui/material/Button'
export function MuiButton() {
return(
<Button>asdf</Button>
)}
then your main.ts
export { Button } from './components/Button'
export { Input } from './components/Input'
export { Label } from './components/Label'
export { MuiButton } from './components/MuiButton'
and import MuiButton into the App.tsx
After an
npm run build
andnpm run dev
, I cannot get my MuiButton into the App. It gives a nonsense error in the console.DefaultPropsProvider-BfsOEd9N.js:1837 Uncaught TypeError: _n is not a function
at Fn (DefaultPropsProvider-BfsOEd9N.js:1837:13)
Any advice here would be VERY MUCH appreciated!!
thanks
J
Does it work if you add "@mui/material/Button" to rollupOptions/external? (and have it in the dependencies)
Do you have your code in a public repo?
Hi Andreas,
Thanks for the reply. In creating an example to go into a public repo, I started over from scratch and somehow managed to get it working without needing your suggested change above. shrug I'm still not sure yet what I did differently, still investigating.
NOW this has given me a good example to solve the main problem I am trying to solve, which is passing a MaterialUI theme from the "parent" App to the "child" 3rd library ui elements. But this is a problem outside of the scope of your article :)
thanks,
Jon
This is a fantastic article and gave me the confidence to rebuild my personal component library, cross-country, with vite and upgrade to storybook 7!
I also wanted to consume the library in a NextJS app but instead of separating the CSS and JS, I wanted to compile them into a single JS file which also eliminates the global import css error and in theory one wouldn't have to worry importing any third-party css into the _app file.
NextJS Update
I'm now using this recommended rollup-plugin-css-only approach, and have decided that I'm ok with including simple instructions to simply import the single bundled css file into the NextJS _app.js file since you only have to do this once.
Instead of vite-plugin-lib-inject-css, I'm experimenting with vite-plugin-css-injected-by-js which seems to be what I want but isn't quite working yet. While I have no css errors I still don't see the css being applied when I consume the published library components. They appear unstyled which definitely isn't good. This is the recommended setup:
When I try
vite-plugin-lib-inject-css
with my library, publish it, and then consume it in a non-nextjs app like the Vite react stackblitz example, it works! I can import my components and their css style is maintained.This is the ChatGPT explanation:
The vite-plugin-css-injected-by-js and vite-plugin-lib-inject-css are both Vite plugins that help manage CSS in your JavaScript bundles, but they serve slightly different purposes and work in different ways.
The vite-plugin-css-injected-by-js takes all the CSS generated by the build process and adds it via JavaScript. This results in a single JavaScript file, as the CSS file is not generated separately. This plugin is useful for those who want a single JavaScript file and can be configured to execute the CSS injection before or after your app code
On the other hand, vite-plugin-lib-inject-css is designed to work specifically in library mode. It injects CSS at the top of each chunk file using an import statement. This plugin supports multi-entries build and is especially helpful when building component libraries
Hm, I think injecting css has it's own problems like no control over the order of appearance. Also no further CSS processing is possible downstream... But this might be no problem in you use case.
What do you mean with "you don't have to worry importing any third-party css into the _app file."?
One question I have:
If my component depends on another npm module (say lodash), when I run vite...the dependency would be bundled into my index.js file....
So do I have lodash listed as a dependency in package.json? If so, wouldn't that mean consumers would transitively install lodash unnecessarily?
So seems like I should either:
external
Hi Kyle, sorry for my late reply. Here is an answer to a similar question if you are still interested: reddit.com/r/reactjs/comments/15o4...
My recommendation for your case would be to add it to dependencies and define it as external.
I made a branch to show how I would do it here:
github.com/receter/my-component-li...
Hey! This is great and got us almost there in terms of building a new standalone consumable component library, so much thanks!!!
The one issue I'm running into is react compatibility - we spun up a new react library using react 18, however, the consumers are react 17, and unknown if/when we can bump those versions up.
Would we need to pin the react version on the library to 17? I'm not sure how other libraries do it, tbh, but running into a roadblock here, unless we revert the version to react 17 on the library; how would we set this up so that it's compatible with both React 17 and React 18 applications?
H Victor,
glad to hear my article was of help. I would try to go with:
I don't know if removing
react/jsx-runtime
is necessary, did you try if it works without removing?And of course you need to take care your component code works with both, React 17 and 18.
If you can, I would advise you to update your consumers to React 18 instead, if possible.
I ended up doing this instead (I saw this range used in another popular library)
and yes, tested it without including
react/jsx-runtime
and this is the error i got:adding it back in resolved this error
FYI - updating the
peerDependencies
to something compatible to 17, as well as removing thereact/jsx-runtime
from the external rollup options ended up fixing this for me. not sure if there's a better way!Any ideas on why I would get an error TS2307: Cannot find module './styles.module.css' or its corresponding type declarations?
Do you have a repo to reproduce? Sometimes I get this in VSCode and in this case a restart of VSCode helps 😅…
It's not finding the vite environment types. In the repo, this is performed by referencing the vite-env.d.ts file. In the comment I just added above I have suggested another, cleaner way to reference the types: -
dev.to/u4ea/comment/2eo2i
I tried installing the
vite-plugin-lib-inject-css
on my Mac M1 (Sonoma 14.4.1) but got error@ast-grep/napi-darwin-x64@npm:0.21.4: No candidates found
For those with the same error, I've found this one that worked for me: npmjs.com/package/vite-plugin-css-...
Great post, thx
hi,I find this article extremely useful. I've translated it into Chinese to help fellow developers better understand it in a localized context.
如果你正在管理多个 React 应用,并希望在ui保持一致,迟早你会发现需要一个组件库。
当我第一次想要创建一个 React 组件库时,花了很多时间 才找到一个满足我所有要求 且不太复杂的设置。
本篇指南可以为我节省大量与这些东西搏斗的精力,我也希望它能帮助到你。
此文章涵盖了 React 组件库 的编写和发布,包括配置构建过程和将包发布到 npm!。
我已经尽力保持所有配置简单明了,尽可能使用默认设置。
完成后,你可以像安装任何其他 npm 包一样安装你的库:
并像这样使用它:
开始之前
在深入实现细节之前,我想详细说明一些关于库设置的技术细节。
🌳 Tree shaking
对我来说,特别重要的一点是:“最终的应用程序里只包含真正必要的代码”。当你导入一个组件时,它只包含必要的 JS 和 CSS 样式,干净利落,很酷吧?
🦑 Css Modules
组件使用 CSS Modules 来写样式。在打包成库时,它们会被编译成普通 CSS 文件,因此使用方完全不需要支持 CSS Modules。
额外的好处是:把 CSS Modules 提前编译后,就绕过了兼容性问题,既可直接引入这个包,零配置。
🧁 如果你想用 vanilla-extract 而不是 CSS Modules,在文章底部有对应的示例,可以参考。
😎 TypeScript
虽然这个库是用 TypeScript 写的,但普通 JavaScript 项目也能无缝使用。
如果你还没试过 TypeScript,不妨尝试下Ts:它不仅能逼你写更干净的代码,还能让你的 AI 小助手给出更好的建议 😉
好了,读够了,现在让我们开始享受乐趣吧!
1.新建 Vite 项目
如果你从未用过 Vite,可以将其视为 Create React App 的替代品。只需几个命令,你就可以开始了。
就是这样,你的新 Vite/React 项目已经准备就绪。
这里有两件我建议你在安装 Vite 后立即做的事情。
2. 基本构建设置
现在你可以运行 npm run dev 并访问 Vite 提供的 URL。
在开发库时,这个环境可以轻松导入你的库并实时查看组件效果(请将 src 内的内容均视为演示页面 example or demo)。
而咱们的库的代码将存放在另一个文件夹中,比如这里我们创建一个名为 lib 的文件夹(也可选用其它名称,但 lib 是个大家都常用的的选择)。
库的主要入口点将是
lib
内名为main.ts
的文件。安装库时,你可以导入从此文件导出的所有内容。Vite 库模式
当前,如果您运行
npm run build
,Vite 会默认将src
目录下的代码构建并输出到dist
文件夹。这是 Vite 的标准行为。不过,目前我们
src
目录中的内容仅用于开发环境下启动和演示,因此并没有被构建的必要。相反,lib
目录中的代码才是我们真正需要构建、编译并发布(到 npm) 的部分!这正是 Vite 库模式 发挥作用的地方。该模式专为构建库(Library)而设计。要激活此模式,只需在
vite.config.ts
配置文件中指定您的库入口点即可。📘 Library 模式文档
📘 lib 模式文档
TypeScript 和库模式
Vite 创建的
tsconfig.json
只包含src
文件夹。要为你新创建的lib
文件夹也启用 Ts,你需要将其添加到 Ts 配置文件中虽然需要为
src
和lib
文件夹都启用 Ts,但在构建库时最好不包含src
。为确保在构建过程中只包含
lib
目录,你可以专门为构建创建一个单独的 Ts 配置文件。唯一的区别是 📜tsconfig.lib.json 中的构建配置只包含
lib
目录,而默认配置包含lib
和src
要使用
tsconfig.lib.json
进行构建,你需要在 package.json 的构建脚本中将配置文件传递给tsc
:最后,你还需要将文件
vite-env.d.ts
从src
复制到lib
。没有这个文件,Ts 在构建时会错过 Vite 提供的一些类型定义(因为我们不再包含src
)。现在你可以再次执行
npm run build
,这是你将在 dist 文件夹中看到的内容:文件
vite.svg
在你的dist
文件夹中,因为 Vite 将public
目录中的所有文件复制到输出文件夹。让我们禁用此行为:构建类型
由于这是一个 Ts 库,你还希望随包一起发布类型定义。幸运的是,有一个 Vite 插件可以做到这一点:vite-plugin-dts
默认情况下,
dts
会为src
和lib
生成类型,因为两个文件夹都包含在项目的.tsconfig
中。这就是为什么我们需要传递一个配置参数:include: ['lib']
。为了测试,让我们向你的库添加一些实际代码。打开
lib/main.ts
并导出一些内容,例如:然后运行
npm run build
来转译你的代码。如果你的dist
文件夹的内容如下所示,你应该已经准备就绪 🥳:3. 没有组件的 React 组件库算什么?
我们做这一切可不仅仅只是为了导出一个
helloAnything
函数😔,所以让我们为我们的库添加一些有意义实质内容:让我们使用三个非常常见的基本组件--按钮、标签和输入框。以及这些组件的非常基本的实现:
最后从库的主文件中导出组件:
如果你再次运行
npm run build
,你会注意到编译后的文件my-component-library.js
现在有 78kb 😮上面组件的实现包含 React JSX 代码,因此
react
(和react/jsx-runtime
)也被打包了。不过由于这个库 将会在已经安装了 React 的项目中使用,你可以将这些依赖项外部化(即你的react项目已经包含了react运行时库),以从包中移除代码:
4. 添加一些样式
如开头所述,这个库将使用 Css Module 来为组件设置样式。
Vite 默认支持 Css Module,你所要做的就是创建以
.module.css
结尾的 CSS 文件即可。并添加一些基本的 CSS 类:
然后在你的组件中导入/使用它们,例如:
⛴️ 发布你的样式
转译库后,你会注意到分发文件夹中有一个新文件:
但这个文件有两个问题:
怎么办?请接着往下看 👇
导入 CSS
由于 CSS 文件无法直接在 JavaScript 中轻松导入,因此需要单独生成 CSS 文件,让库的使用者自行决定如何处理该文件。
但但如果我们假设:“使用该库的应用程序已经配置了能处理 CSS 导入的打包器配置”会怎样?
要实现这种处理方式,就需要要求我们编译后 Js 包必须包含 CSS 文件的导入语句,这里我们将使用另一个 Vite 插件 👉 vite-plugin-lib-inject-css 来达到这个效果,且零配置。
构建库 并 查看打包的 Js 文件(
dist/my-component-library.js
)的顶部,你会发现 OBJK成了😊!拆分 CSS
接下来我们解决第二个问题:当你从库中导入某些内容时,
main.css
也会被导入,所有 CSS 样式最终都会出现在你的应用程序中,即使你只导入Button
这个组件。好在
libInjectCSS 插件
会为每个组件代码块生成独立的 CSS 文件,并在每个代码块输出文件的开头添加对应的导入语句。因此,如果您对 Js 代码进行拆分,就将会得到独立的 CSS 文件——且这些样式文件只会在对应的 JavaScript 文件被导入时才会同步加载,这就是解决方案!
实现此功能的一种方法是将每个文件都转换为 Rollup 的入口点。更赞的是,Rollup 文档中正好推荐了这种实现方式:
所以让我们将此添加到你的配置中。
首先安装
glob
,因为它是必需的:然后将你的 Vite 配置更改为:
现在你的
dist
文件夹根目录中会有一堆 Js 和 CSS 文件。它可以工作,但看起来不是特别美观,不是吗?重新打包lib,你会发现所有 Js 文件现在应该与它们的类型定义一起位于你在
lib
中创建的相同有组织的文件夹结构中。CSS 文件位于名为 assets 的新文件夹内。🙌注意 主文件的名称已从"my-component-library.js"更改为"main.js",真棒👍!
4. 发布包之前的最后几个步骤
你的构建设置现在已经准备就绪,在发布包之前只需要考虑几件事。
package.json
文件将与你的包文件一起发布。你需要确保它包含有关包的所有重要信息。主文件
每个 npm 包都有一个主要入口点,默认情况下这个文件是包根目录中的
index.js
。你的库的主要入口点现在位于
dist/main.js
,因此需要在你的package.json
中设置。类型的入口点也是如此:dist/main.d.ts
定义要发布的文件
你还应该定义哪些文件应该打包到你的分发包中。
依赖项
现在看看你的
dependencies
:现在应该只有两个react
和react-dom
以及一些devDependencies
。你也可以将这两个移动到
devDependencies
中。并且另外将它们添加为peerDependencies
,以便使用应用程序知道它必须安装 React 才能使用此包。副作用
为了防止 CSS 文件被消费者的 tree-shaking 工作意外删除,你还应该将生成的 CSS 指定为副作用:
你可以在 webpack 文档中阅读有关
sideEffects
的更多信息。(最初来自 Webpack,此字段已发展为现在也受其他打包器支持的通用模式)确保包已构建
你可以使用特殊的生命周期脚本
prepublishOnly
来保证在发布包之前始终构建你的更改:5. 演示页面和部署
要在演示页面上使用你的组件,你可以简单地直接从项目根目录导入组件。这是可行的,因为你的
package.json
指向转译后的主文件dist/main.ts
。要发布你的包,你只需要运行
npm publish
。如果你想将包发布给公众,你必须在package.json
中设置private: false
。你可以在我的这些文章中阅读有关发布包的更多信息,包括在本地项目中安装它(不发布):
发布和安装你的包
利用 GitHub actions自动构建和发布
Thank you for this amazing tutorial on setting up a React component library. I created a component library, but component props were not showing in IntelliSense. After reading your post, I found my mistake: I exported both the es and umd builds, which caused the issue. After using only the es build, the problem was resolved.
Thanks again.
Thank you so much for this article! I was trying to avoid CSS-in-JS since I use CSS modules heavily. The only tweaks I needed (and commented on below) are fixing the
dts
configuration:There is a branch "revision-1" where I did a similar thing. I still need to update the article based on this branch.
You can create a
tsconfig.lib.json
:tsconfig.lib.json
:Then remove the
include: ['lib'],
from the dts configuration and adapt also the build script to"build": "tsc -b ./tsconfig.lib.json && vite build",
This way the config is more concise.Why did you want/need to set
rollupTypes: true
?Thank you very much for the detailed tutorial - it is the best I could find in the internet for this subject.
Vite 5 introduces changes that does not align perfectly with this tutorial (that uses vite 4), most of the things do match to what is written here, but there are few minor changes.
So I created an updated version that uses Vite 5, for the public to enjoy:
Npm:
npmjs.com/package/template-react-c...
GitHub:
github.com/ben-sembira-1/react-npm...
Note: It does not contain css modules and storybook, but I assume those hasn't changed between vite versions. If someone tries and faces a difference, please make a pull request or at least let me know!
Hello!
Thanks for the tutorial! It is great! Unfortunately I was not able fully finish it. After I added glob config to my vite.config.ts, the "npm run build" started to return an error:
failed to load config from ..../vite.config.ts
error during build:
file:///..../vite.config.ts.timestamp-1694619764411-22e8f4327550d.mjs:7
import glob from "file:///..../node_modules/glob/dist/mjs/index.js";
^^^^
SyntaxError: The requested module 'file:///..../node_modules/glob/dist/mjs/index.js' does not provide an export named 'default'
at ModuleJob._instantiate (node:internal/modules/esm/module_job:122:21)
at async ModuleJob.run (node:internal/modules/esm/module_job:188:5)
at async DefaultModuleLoader.import (node:internal/modules/esm/loader:228:24)
at async loadConfigFromBundledFile (file:///..../node_modules/vite/dist/node/chunks/dep-df561101.js:66235:21)
at async loadConfigFromFile (file:///..../node_modules/vite/dist/node/chunks/dep-df561101.js:66086:28)
at async resolveConfig (file:///..../node_modules/vite/dist/node/chunks/dep-df561101.js:65682:28)
at async build (file:///..../node_modules/vite/dist/node/chunks/dep-df561101.js:47852:20)
at async CAC. (file:///..../node_modules/vite/dist/node/cli.js:822:9)
I use npm version 9.8.0, nvm version 20.5.1, vite version
Best Regards,
Andrei
Hi Andrei,
thanks for your comment, I am glad you like the tutorial!
I guess you need to do a named import:
It was a "typo" in the article, I have updated it accordingly, thanks for your feedback!
Hello Andreas! Thanks a lot for the update! now everything works just perfectly!
thanks for excellent article and sample code. The import CSS does not get transpiled to JS script. Thus I get run time error (when I use the library in another project).
For eg. Button/index.js has the following:
import "../../assets/index4.css";
Obvisiouly, this will not work in the brower. Should'nt this get transpiled as CSS Module ?
If the consuming environment does not have a bundler setup that supports CSS imports please see this answer: dev.to/receter/comment/2a198
You can disable these CSS imports and generate a CSS file that you can import sepparately.
thank you. Got it!
Hi Andreas, great article!
I have an issue with the global css import, I have imported global styles in my main.ts file and when the library is compiled I can see those clases added into my /assets/main.css file that is imported into the compiled main.js.
The problem is when I import the library in a create-react-app project, it ignores everything that is inside main.css, It only works if I directly import it into the project like: import "mylibrary/dist/assets/main.css".
Do you know how to solve this issue?
Hi Jonn,
do you import anything in the create-react-app project or did you just install the plugin? The import will only work if the
main.js
file is somehow included in the build. For example when you import anything frommain.js
.Do you have an example project on GitHub in which I can replicate the issue?
I have a interface i export in a component. When i run build i get a error that it can not import the interface in the main.ts. Why is that?
Hi, can you provide a repo to reproduce this? What is the error message?
Have a component in lib/components/date-input/
It has a date-input.component.tsx file that includes a interface:
It also has a index.ts file with the following code:
When I run build I get this error:
Is it maybe related to this issue? github.com/rollup/plugins/issues/71
If you can share a repo to reproduce the issue, I can have a look.
Hello, after adding tailwind to my project, the imports are not working when the developed lib is used in another project. It is generating a different structure in the "dist" directory
Hey! Amazing tutorial thanks for sharing.
Im having kind of an issue where the build is creating an additional file and Im not sure what could be happening.
Any ideas? Not sure where i should look
After building my component library using your guide, it works perfectly. However, when running vitest I get an error saying the .css files are unable to be located. Is this expected? I have tried configuring my vitest settings, but I feel like the workaround I have produced is better solved for this library itself. (The components render and work fine in my project, but the tests that use anything from the component library have a .css file error)
I think a few others in this thread have experienced this issue
Hm, do you test the original files or the transpiled code?
If you have a public example repo I can take a look at your issue.
I would need more detailed information to give you an helpful answer. Do you have a repo were this happens? Does it also happen when you install my demo npm library? (link at the bottom of the article) What bundler setup are you using?
For the
main.d.ts
to generate, I've had to calldts
as follows invite.config.ts
:Which version of dts did you use/install?
"vite": "^5.4.9"
"vite-plugin-dts": "^4.3.0"
I have some .ts files which contains interfaces and types in it. After I build library those are getting converted into .d.ts files (which is expected). But I'm not able to import types from these d.ts files into my Client Application. getting below error.
[plugin:vite:import-analysis] Failed to resolve import "../dist/types/IHeaderConfigs" from "client/main.tsx". Does the file exist?
Hi Pankaj, I can help you if you provide me with a repo to reproduce this issue.
Wow, @receter ! Absolutely awesome stuff! That's exactly what I needed. Thank you! Can I buy you a coffee? :D
I have my library as a part of nx monorepo with Vite as bundler and my consuming app is in Next.js v13.5.6 - works flawlessly! I used plain old sass with BEM and had css code split up per component and loaded per component - Next.js deals with that easily - no css / font 'jumps'.
Cool, glad my article did help you! If you ever make it to Austria/Graz you can buy me a real coffee 😉
Terrific article, thank you!
I had an issue with changes in my library not being reflected in the consuming project. It turned out it was because there was a cached version in node_modules/.vite
Deleting that folder solved the problem. For more information on the cache, see Vite: dep-pre-bundling - caching.
Hope that helps someone else!
Thank you for this article its been extremely helpful! Quick question, how much refactor would it be to substitute css modules with tailwind? Do you have any resources you could point me too?
I think that should be fairly easy. I never worked with tailwind but always wanted to try using it. If I have time today, I will give it a try and push a branch with tailwind instead of CSS Modules.
How can you add global styles to the library and also the Storybook docs? Global styles like: * { box-sizing: border-box; }
I would recommend to ship a css file as part of the package and import in manually in the consuming applications. Other than that you should be able to just import a CSS file in
main.ts
. Just be aware that it will then be only imported if any of the components exported inmain.ts
are imported in the consuming application.Thank you. That's actually the behavior I want, so the components' styles don't conflict with any other style the end user may have in their consuming app.
Great post I've been looking for something like this for a while now...is it possible to test this by using npm link with another react app locally before deploying to npm?
Hi, yes you can totally do that. There are some peculiarities when using
npm link
. Here is an older article of mine, the section about npm link might help you: dev.to/receter/the-minimal-setup-t...In most cases it might be better to just install the package via a file path, see stackoverflow.com/questions/143818...
Like so:
@receter , thanks so much for this. Fantastic walkthrough!
Thanks bro! You've just saved my life. I got a task at work to implement UI library and struggled for a couple of days until encountered your solution. Liked and subscribed!
I'm glad that helped you, let me know how it goes! I am currently working on a library to help creating component libraries, but it is very much experimental and work in progress. If you want to take a look for inspiration, you can find it here: github.com/receter/sys42/
Thanks @receter, this tutorial was very easy to follow. I've also bookmarked it for the future.
Thank for a great article, it's really helpful.
Have you successfully added vanilla extract to this project?
Here is a branch with vanilla-extract: github.com/receter/my-component-li...
I am stuck with CSS Modules. Still I like the idea of vanilla extract. I think it should just work, but yeah I should try it :)
The types are not getting exported correctly. Have created a stackoverflow issue. Any help is much appreciated. Thanks.
Do you have a link to the Stackoverflow issue?
Is there a reason why my package.json is not being included when my library is built?
Hi, your
package.json
is not copied to thedist
folder. It stays where it is and is shipped as it is when you runnpm publish
in the root directory. Just take a look at npmjs.com/package/@receter/my-comp... to see what is published in my example.Fantastic Explanation. Thank you.
good!
I heard Vite library doesnt do tree shake, how do you know your import is being tree shaked?
Thanks for your comment. I will investigate this.
Here is a SO question I found regarding this topic: stackoverflow.com/questions/743626...
And this linked issue in particular: github.com/vitejs/vite/issues/5174
It depends on the bundler I guess, seems that when using webpack you might need to do additional work.
To find out if tree shaking works, take a look at the final bundle.
Using some unique strings in your code can help you to quickly find if something made it to the bundle.
I will do some tests to find out what works and will share the repos in f I do so.
If you have a public repo to reproduce tree shaking issues I am also happy to have a look at it.
how to write styles using tailwind
it would be nice if you include that to
I think you can just use tailwind classes an make sure tailwind is set up in the consuming application and your test environment.
Hey Andreas! Nice work dude, u really help me! Is really a great article.
You have something about make a lib with more performance? My lib is getting bigger and bigger.
With performance you mean build performance?
What other steps do I need for developing component library for React, Vue, HTML all at the same time?
I think this library only supports React.
You can look into developing WebComponents developer.mozilla.org/en-US/docs/W...
Another option is to create a monorepo with individual packages for each library.
Hi Andrey, thanks for sharing. I followed the steps and its working great. I have one concern regarding tailwind css. How the consumer app will add the css as it building in build time
Hi, i have implemented the structure suggested. Thanks for sharing. While debugging the consumer app i noticed the components i added are minimized. Can i debug the lib content somehow?
Thank you