<< back

Building Component Libraries

Rule Number One: Don't.

JK but not really. Fr tho you should probably ask yourself: do I really need it?

It's very easy conclusion to get to to think ya it's a great idea to have a package that has all my common components and utilities. However, achieving that sometimes can be tricky. Now I am going to walk through some of the battles and struggles at my current company to truly make our component library useful and well packaged.

Background

When I pulled up the new job, on the frontend side we have 3 packages. A component library, and two consuming parties - the landing page web server (on NextJS) and the app itself (React SPA, with CRA). Very decent setup but code quality and the way things are built was not very clean. There are a lot of problems like repeated code, duplicated tailwind configs in all 3 packages and such. Everything still works together but sort of by accident.

So my first major infrastructure level project was to sort out all this mess on the frontend side.

Vite Migration

The first thing I wanted to do was a Vite migration. I want to move our component library and the SPA web application to Vite. Does this bring immediate value? Not really but I think most of us can agree CRA and webpack is not the way to go in 2025. Plus, I want to unify the building process of our component library and web app more as the component library was on rollup - nothing wrong with it but I know Vite's library mode is much simpler.

Then there came the first issue - the UNTRANSPILED process.env. Our component library package has some process.env usages that's not transpiled or evaluated during build and gets directly consumed by the upstream landing page web server and the web app. For example, both landing page web server and the web app consumes some common amplitude utility functions from the component library and the way amplitude token was set up was by setting up environment variables in the UPSTREAM packages. So symptom is we have some process.env that cannot be processed by Vite but the root cause was more of a code abstraction level issue. So I did some refactors to remove those process.env usage in component library, set it up with the library mode and called it.

And this was also why I said things were working together sort of by accident - component library vending code with process.env in it and both upstreams happened to be able to process these process.env usages. We were just lucky ;)

After component library was in a better shape, moving upstream web app to Vite was much easier. It was a lot of work but nothing tricky. Though I still shipped a few CSS related bugs because of missed refactor and such.

Tailwind Isolation

The second major issue I ran into was a performance issue due to our bad practice of using tailwindCSS. All of our frontend packages use tailwind and the way we make them work together is by two janky maneuvers

  1. Component library does NOT generate tailwind css classes on build.
  2. Consuming party tailwind scans the javascript bundle from component library to generate tailwind css classes needed.

In practice, this is what part of the tailwind config looks like in the consuming party. See that later deleted line

old tailwind config

This setup causes the initial page load to be around 80 seconds even though the Vite dev server is ready within a second.

To root cause this, I also had to run the browser to eventually figure it out. See below screenshot.

tailwind scan performance

To solve this, we decided letting the consuming parties scan component library dist files is a no go. What we need is to figure out a way to make component library bundle its own tailwind CSS while NOT interfering with upstream. Fortunately, this is exactly what tailwind prefix is designed for. Here is a good read from Kevin Yank on using tailwind prefix to avoid clashing.

With some more refactor and below tailwind entry css file in the component library, we finally correctly isolated tailwind css needed for component library away from its consumers. See below tailwind entry in component library too.

/* @tailwind base; DO NOT import base rules as doubling base rules would cause issues with consumer parties */
@tailwind components;
@tailwind utilities;

Remove React DaisyUI

The final thing with this effort is to remove React DaisyUI. The reason is simply React DaisyUI does not support tailwind prefix. In order to complete the effort to isolate our component library tailwind CSS from the consumer parties' CSS. We have to move away from React DaisyUI.

This is not hard to do as we don't really rely on DaisyUI and headlessUI is a much better option. But I guess the lesson here is to learn about the limitation of the component library you rely on, especially when you are vending out other component library through yours.

Conclusion

After all 3 things I did, our component library becomes truly well encapsulated. I want to say it's well packaged but it's probably not there yet. There is a lot of work yet to be done.